diff --git a/docs/projects.sh b/docs/projects.sh index c91ab03..d2a5f13 100644 --- a/docs/projects.sh +++ b/docs/projects.sh @@ -139,3 +139,44 @@ curl --location 'https://land.dbiz.com//api/method/upload_file' \ --form 'docname="p9ti8veq2g"' \ --form 'optimize="true"' +#get detail of a project +curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_detail' \ +--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 '{ + "name": "#DA00011" +}' + +#response +{ + "message": { + "success": true, + "data": { + "name": "#DA00011", + "designed_area": "f67gg7", + "address_of_project": "7fucuv", + "project_owner": "cycu", + "design_firm": null, + "contruction_contractor": null, + "design_area": 2585.0, + "products_included_in_the_design": "thy", + "project_progress": "k1mr565o91", + "expected_commencement_date": "2025-11-30", + "description": null, + "request_date": "2025-11-27 16:51:54", + "workflow_state": "Pending approval", + "reason_for_rejection": null, + "status": "Chờ phê duyệt", + "status_color": "Warning", + "is_allow_modify": true, + "is_allow_cancel": true, + "files_list": [ + { + "name": "0068d2403c", + "file_url": "https://land.dbiz.com/private/files/image_picker_32BD79E6-7A71-448E-A5DF-6DA7D12A1303-66894-000015E4259DBB5B.png" + } + ] + } + } +} \ No newline at end of file diff --git a/lib/core/constants/api_constants.dart b/lib/core/constants/api_constants.dart index c1ee70c..0cbb3d4 100644 --- a/lib/core/constants/api_constants.dart +++ b/lib/core/constants/api_constants.dart @@ -308,6 +308,13 @@ class ApiConstants { static const String saveProject = '/building_material.building_material.api.project.save'; + /// Get project detail (requires sid and csrf_token) + /// POST /api/method/building_material.building_material.api.project.get_detail + /// Body: { "name": "#DA00011" } + /// Returns: Full project detail with all fields + static const String getProjectDetail = + '/building_material.building_material.api.project.get_detail'; + /// Create new project (legacy endpoint - may be deprecated) /// POST /projects static const String createProject = '/projects'; diff --git a/lib/core/network/api_interceptor.g.dart b/lib/core/network/api_interceptor.g.dart index 27cfe02..a21528d 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'4d3147e9084d261e14653386ecd74ee471993af4'; + r'6afa480caa6fcd723dab769bb01601b8a37e20fd'; /// Provider for ErrorTransformerInterceptor diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index ccfd821..1580777 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -42,6 +42,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/domain/entities/project_submission.dart'; import 'package:worker/features/projects/presentation/pages/submission_create_page.dart'; import 'package:worker/features/projects/presentation/pages/submissions_page.dart'; import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart'; @@ -391,12 +392,17 @@ final routerProvider = Provider((ref) { MaterialPage(key: state.pageKey, child: const SubmissionsPage()), ), - // Submission Create Route + // Submission Create/Edit Route GoRoute( path: RouteNames.submissionCreate, name: RouteNames.submissionCreate, - pageBuilder: (context, state) => - MaterialPage(key: state.pageKey, child: const SubmissionCreatePage()), + pageBuilder: (context, state) { + final submission = state.extra as ProjectSubmission?; + return MaterialPage( + key: state.pageKey, + child: SubmissionCreatePage(submission: submission), + ); + }, ), // Quotes Route diff --git a/lib/features/projects/data/datasources/submissions_remote_datasource.dart b/lib/features/projects/data/datasources/submissions_remote_datasource.dart index c65c0e4..2f19c9a 100644 --- a/lib/features/projects/data/datasources/submissions_remote_datasource.dart +++ b/lib/features/projects/data/datasources/submissions_remote_datasource.dart @@ -27,6 +27,10 @@ abstract class SubmissionsRemoteDataSource { int limitPageLength = 0, }); + /// Fetch project detail by name + /// Returns the full project detail as a model + Future getSubmissionDetail(String name); + /// Create or update a project submission /// Returns the project name (ID) from the API response Future saveSubmission(ProjectSubmissionRequest request); @@ -170,6 +174,42 @@ class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource { } } + /// Get project detail by name + /// + /// Calls: POST /api/method/building_material.building_material.api.project.get_detail + /// Body: { "name": "#DA00011" } + /// Response: { "message": { "success": true, "data": {...} } } + /// Returns: Full project detail as model + @override + Future getSubmissionDetail(String name) async { + try { + final response = await _dioClient.post>( + '${ApiConstants.frappeApiMethod}${ApiConstants.getProjectDetail}', + data: {'name': name}, + ); + + final data = response.data; + if (data == null) { + throw Exception('No data received from getProjectDetail API'); + } + + // API returns: { "message": { "success": true, "data": {...} } } + final message = data['message'] as Map?; + if (message == null) { + throw Exception('No message field in getProjectDetail response'); + } + + final detailData = message['data'] as Map?; + if (detailData == null) { + throw Exception('No data field in getProjectDetail response'); + } + + return ProjectSubmissionModel.fromJson(detailData); + } catch (e) { + throw Exception('Failed to get project detail: $e'); + } + } + /// Save (create/update) a project submission /// /// Calls: POST /api/method/building_material.building_material.api.project.save @@ -227,7 +267,7 @@ class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource { final fileName = filePath.split('/').last; final formData = FormData.fromMap({ 'file': await MultipartFile.fromFile(filePath, filename: fileName), - 'is_private': '1', + 'is_private': '0', 'folder': 'Home/Attachments', 'doctype': 'Architectural Project', 'docname': projectName, diff --git a/lib/features/projects/data/models/project_submission_model.dart b/lib/features/projects/data/models/project_submission_model.dart index 0d2cf1a..a40d6d4 100644 --- a/lib/features/projects/data/models/project_submission_model.dart +++ b/lib/features/projects/data/models/project_submission_model.dart @@ -1,47 +1,117 @@ /// 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 +/// Data model for project submission from API responses. +/// Based on API response from building_material.building_material.api.project.get_detail library; -import 'package:hive_ce/hive.dart'; -import 'package:worker/core/constants/storage_constants.dart'; import 'package:worker/features/projects/domain/entities/project_submission.dart'; -part 'project_submission_model.g.dart'; +/// Project File Model +class ProjectFileModel { + /// Unique file identifier (API: name) + final String id; -/// Project Submission Model - Type ID: 14 -@HiveType(typeId: HiveTypeIds.projectSubmissionModel) -class ProjectSubmissionModel extends HiveObject { + /// Full URL to the file (API: file_url) + final String fileUrl; + + const ProjectFileModel({ + required this.id, + required this.fileUrl, + }); + + /// Create from JSON (API response) + factory ProjectFileModel.fromJson(Map json) { + return ProjectFileModel( + id: json['name'] as String, + fileUrl: json['file_url'] as String, + ); + } + + /// Convert to JSON + Map toJson() { + return { + 'name': id, + 'file_url': fileUrl, + }; + } + + /// Convert to entity + ProjectFile toEntity() { + return ProjectFile( + id: id, + fileUrl: fileUrl, + ); + } + + /// Create from entity + factory ProjectFileModel.fromEntity(ProjectFile entity) { + return ProjectFileModel( + id: entity.id, + fileUrl: entity.fileUrl, + ); + } +} + +/// Project Submission Model +class ProjectSubmissionModel { /// Unique submission identifier (API: name) - @HiveField(0) final String submissionId; /// Project name/title (API: designed_area) - @HiveField(1) final String designedArea; /// Design area value in square meters (API: design_area) - @HiveField(2) final double designArea; /// Submission/request date (API: request_date) - @HiveField(3) final DateTime requestDate; /// Status label - Vietnamese (API: status) - @HiveField(4) final String status; /// Rejection reason if rejected (API: reason_for_rejection) - @HiveField(5) final String? reasonForRejection; /// Status color indicator (API: status_color) - @HiveField(6) final String statusColor; - ProjectSubmissionModel({ + /// Project address (API: address_of_project) + final String? addressOfProject; + + /// Project owner name (API: project_owner) + final String? projectOwner; + + /// Design firm name (API: design_firm) + final String? designFirm; + + /// Construction contractor name (API: contruction_contractor) + final String? constructionContractor; + + /// Products included in the design (API: products_included_in_the_design) + final String? productsIncludedInTheDesign; + + /// Project progress ID reference (API: project_progress) + final String? projectProgress; + + /// Expected commencement date (API: expected_commencement_date) + final DateTime? expectedCommencementDate; + + /// Project description (API: description) + final String? description; + + /// Workflow state (API: workflow_state) + final String? workflowState; + + /// Whether the submission can be modified (API: is_allow_modify) + final bool isAllowModify; + + /// Whether the submission can be cancelled (API: is_allow_cancel) + final bool isAllowCancel; + + /// List of attached files (API: files_list) + final List filesList; + + const ProjectSubmissionModel({ required this.submissionId, required this.designedArea, required this.designArea, @@ -49,10 +119,39 @@ class ProjectSubmissionModel extends HiveObject { required this.status, this.reasonForRejection, required this.statusColor, + this.addressOfProject, + this.projectOwner, + this.designFirm, + this.constructionContractor, + this.productsIncludedInTheDesign, + this.projectProgress, + this.expectedCommencementDate, + this.description, + this.workflowState, + this.isAllowModify = false, + this.isAllowCancel = false, + this.filesList = const [], }); /// Create from JSON (API response) + /// Handles both list response and detail response formats factory ProjectSubmissionModel.fromJson(Map json) { + // Parse expected_commencement_date + DateTime? expectedDate; + final expectedDateStr = json['expected_commencement_date'] as String?; + if (expectedDateStr != null && expectedDateStr.isNotEmpty) { + try { + expectedDate = DateTime.parse(expectedDateStr); + } catch (_) {} + } + + // Parse files_list + final filesListJson = json['files_list'] as List?; + final filesList = filesListJson + ?.map((f) => ProjectFileModel.fromJson(f as Map)) + .toList() ?? + []; + return ProjectSubmissionModel( submissionId: json['name'] as String, designedArea: json['designed_area'] as String, @@ -61,6 +160,19 @@ class ProjectSubmissionModel extends HiveObject { status: json['status'] as String, reasonForRejection: json['reason_for_rejection'] as String?, statusColor: json['status_color'] as String, + addressOfProject: json['address_of_project'] as String?, + projectOwner: json['project_owner'] as String?, + designFirm: json['design_firm'] as String?, + constructionContractor: json['contruction_contractor'] as String?, + productsIncludedInTheDesign: + json['products_included_in_the_design'] as String?, + projectProgress: json['project_progress'] as String?, + expectedCommencementDate: expectedDate, + description: json['description'] as String?, + workflowState: json['workflow_state'] as String?, + isAllowModify: json['is_allow_modify'] as bool? ?? false, + isAllowCancel: json['is_allow_cancel'] as bool? ?? false, + filesList: filesList, ); } @@ -74,6 +186,19 @@ class ProjectSubmissionModel extends HiveObject { 'status': status, 'reason_for_rejection': reasonForRejection, 'status_color': statusColor, + 'address_of_project': addressOfProject, + 'project_owner': projectOwner, + 'design_firm': designFirm, + 'contruction_contractor': constructionContractor, + 'products_included_in_the_design': productsIncludedInTheDesign, + 'project_progress': projectProgress, + 'expected_commencement_date': + expectedCommencementDate?.toIso8601String(), + 'description': description, + 'workflow_state': workflowState, + 'is_allow_modify': isAllowModify, + 'is_allow_cancel': isAllowCancel, + 'files_list': filesList.map((f) => f.toJson()).toList(), }; } @@ -87,6 +212,18 @@ class ProjectSubmissionModel extends HiveObject { status: status, reasonForRejection: reasonForRejection, statusColor: statusColor, + addressOfProject: addressOfProject, + projectOwner: projectOwner, + designFirm: designFirm, + constructionContractor: constructionContractor, + productsIncludedInTheDesign: productsIncludedInTheDesign, + projectProgress: projectProgress, + expectedCommencementDate: expectedCommencementDate, + description: description, + workflowState: workflowState, + isAllowModify: isAllowModify, + isAllowCancel: isAllowCancel, + filesList: filesList.map((f) => f.toEntity()).toList(), ); } @@ -100,6 +237,19 @@ class ProjectSubmissionModel extends HiveObject { status: entity.status, reasonForRejection: entity.reasonForRejection, statusColor: entity.statusColor, + addressOfProject: entity.addressOfProject, + projectOwner: entity.projectOwner, + designFirm: entity.designFirm, + constructionContractor: entity.constructionContractor, + productsIncludedInTheDesign: entity.productsIncludedInTheDesign, + projectProgress: entity.projectProgress, + expectedCommencementDate: entity.expectedCommencementDate, + description: entity.description, + workflowState: entity.workflowState, + isAllowModify: entity.isAllowModify, + isAllowCancel: entity.isAllowCancel, + filesList: + entity.filesList.map((f) => ProjectFileModel.fromEntity(f)).toList(), ); } } diff --git a/lib/features/projects/data/models/project_submission_model.g.dart b/lib/features/projects/data/models/project_submission_model.g.dart deleted file mode 100644 index 168f84a..0000000 --- a/lib/features/projects/data/models/project_submission_model.g.dart +++ /dev/null @@ -1,60 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'project_submission_model.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class ProjectSubmissionModelAdapter - extends TypeAdapter { - @override - final typeId = 14; - - @override - ProjectSubmissionModel read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return ProjectSubmissionModel( - submissionId: fields[0] 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(7) - ..writeByte(0) - ..write(obj.submissionId) - ..writeByte(1) - ..write(obj.designedArea) - ..writeByte(2) - ..write(obj.designArea) - ..writeByte(3) - ..write(obj.requestDate) - ..writeByte(4) - ..write(obj.status) - ..writeByte(5) - ..write(obj.reasonForRejection) - ..writeByte(6) - ..write(obj.statusColor); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ProjectSubmissionModelAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/lib/features/projects/data/repositories/submissions_repository_impl.dart b/lib/features/projects/data/repositories/submissions_repository_impl.dart index 185473c..0d3c763 100644 --- a/lib/features/projects/data/repositories/submissions_repository_impl.dart +++ b/lib/features/projects/data/repositories/submissions_repository_impl.dart @@ -138,6 +138,16 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository { } } + @override + Future getSubmissionDetail(String name) async { + try { + final model = await _remoteDataSource.getSubmissionDetail(name); + return model.toEntity(); + } catch (e) { + rethrow; + } + } + @override Future saveSubmission(ProjectSubmissionRequest request) async { try { diff --git a/lib/features/projects/domain/entities/project_submission.dart b/lib/features/projects/domain/entities/project_submission.dart index ae04bba..fc38e77 100644 --- a/lib/features/projects/domain/entities/project_submission.dart +++ b/lib/features/projects/domain/entities/project_submission.dart @@ -1,22 +1,53 @@ /// 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 +/// Based on API response from building_material.building_material.api.project.get_detail library; import 'package:equatable/equatable.dart'; +/// Project File Entity +/// +/// Represents an uploaded file attached to a project submission. +class ProjectFile extends Equatable { + /// Unique file identifier (API: name) + final String id; + + /// Full URL to the file (API: file_url) + final String fileUrl; + + const ProjectFile({ + required this.id, + required this.fileUrl, + }); + + @override + List get props => [id, fileUrl]; +} + /// Project Submission Entity /// /// Contains information about a completed project submission. /// Mapped from API response: /// - name -> submissionId /// - designed_area -> designedArea (project name/title) +/// - address_of_project -> addressOfProject +/// - project_owner -> projectOwner +/// - design_firm -> designFirm +/// - contruction_contractor -> constructionContractor /// - design_area -> designArea (area value in m²) +/// - products_included_in_the_design -> productsIncludedInTheDesign +/// - project_progress -> projectProgress (ID reference) +/// - expected_commencement_date -> expectedCommencementDate +/// - description -> description /// - request_date -> requestDate -/// - status -> status (Vietnamese label) +/// - workflow_state -> workflowState /// - reason_for_rejection -> reasonForRejection +/// - status -> status (Vietnamese label) /// - status_color -> statusColor +/// - is_allow_modify -> isAllowModify +/// - is_allow_cancel -> isAllowCancel +/// - files_list -> filesList class ProjectSubmission extends Equatable { /// Unique submission identifier (API: name) final String submissionId; @@ -24,31 +55,80 @@ class ProjectSubmission extends Equatable { /// Project name/title (API: designed_area) final String designedArea; + /// Project address (API: address_of_project) + final String? addressOfProject; + + /// Project owner name (API: project_owner) + final String? projectOwner; + + /// Design firm name (API: design_firm) + final String? designFirm; + + /// Construction contractor name (API: contruction_contractor) + final String? constructionContractor; + /// Design area value in square meters (API: design_area) final double designArea; + /// Products included in the design (API: products_included_in_the_design) + final String? productsIncludedInTheDesign; + + /// Project progress ID reference (API: project_progress) + final String? projectProgress; + + /// Expected commencement date (API: expected_commencement_date) + final DateTime? expectedCommencementDate; + + /// Project description (API: description) + final String? description; + /// Submission/request date (API: request_date) final DateTime requestDate; + /// Workflow state (API: workflow_state) + /// e.g., "Pending approval", "Approved", "Rejected", "Cancelled" + final String? workflowState; + + /// Rejection reason if rejected (API: reason_for_rejection) + final String? reasonForRejection; + /// Status label - Vietnamese (API: status) /// e.g., "Chờ phê duyệt", "Đã được phê duyệt", "Từ chối", "HỦY BỎ" final String status; - /// Rejection reason if rejected (API: reason_for_rejection) - final String? reasonForRejection; - /// Status color indicator (API: status_color) /// Values: "Warning", "Success", "Danger" final String statusColor; + /// Whether the submission can be modified (API: is_allow_modify) + final bool isAllowModify; + + /// Whether the submission can be cancelled (API: is_allow_cancel) + final bool isAllowCancel; + + /// List of attached files (API: files_list) + final List filesList; + const ProjectSubmission({ required this.submissionId, required this.designedArea, + this.addressOfProject, + this.projectOwner, + this.designFirm, + this.constructionContractor, required this.designArea, + this.productsIncludedInTheDesign, + this.projectProgress, + this.expectedCommencementDate, + this.description, required this.requestDate, - required this.status, + this.workflowState, this.reasonForRejection, + required this.status, required this.statusColor, + this.isAllowModify = false, + this.isAllowCancel = false, + this.filesList = const [], }); /// Check if submission is pending approval @@ -64,20 +144,44 @@ class ProjectSubmission extends Equatable { ProjectSubmission copyWith({ String? submissionId, String? designedArea, + String? addressOfProject, + String? projectOwner, + String? designFirm, + String? constructionContractor, double? designArea, + String? productsIncludedInTheDesign, + String? projectProgress, + DateTime? expectedCommencementDate, + String? description, DateTime? requestDate, - String? status, + String? workflowState, String? reasonForRejection, + String? status, String? statusColor, + bool? isAllowModify, + bool? isAllowCancel, + List? filesList, }) { return ProjectSubmission( submissionId: submissionId ?? this.submissionId, designedArea: designedArea ?? this.designedArea, + addressOfProject: addressOfProject ?? this.addressOfProject, + projectOwner: projectOwner ?? this.projectOwner, + designFirm: designFirm ?? this.designFirm, + constructionContractor: constructionContractor ?? this.constructionContractor, designArea: designArea ?? this.designArea, + productsIncludedInTheDesign: productsIncludedInTheDesign ?? this.productsIncludedInTheDesign, + projectProgress: projectProgress ?? this.projectProgress, + expectedCommencementDate: expectedCommencementDate ?? this.expectedCommencementDate, + description: description ?? this.description, requestDate: requestDate ?? this.requestDate, - status: status ?? this.status, + workflowState: workflowState ?? this.workflowState, reasonForRejection: reasonForRejection ?? this.reasonForRejection, + status: status ?? this.status, statusColor: statusColor ?? this.statusColor, + isAllowModify: isAllowModify ?? this.isAllowModify, + isAllowCancel: isAllowCancel ?? this.isAllowCancel, + filesList: filesList ?? this.filesList, ); } @@ -85,11 +189,23 @@ class ProjectSubmission extends Equatable { List get props => [ submissionId, designedArea, + addressOfProject, + projectOwner, + designFirm, + constructionContractor, designArea, + productsIncludedInTheDesign, + projectProgress, + expectedCommencementDate, + description, requestDate, - status, + workflowState, reasonForRejection, + status, statusColor, + isAllowModify, + isAllowCancel, + filesList, ]; @override diff --git a/lib/features/projects/domain/repositories/submissions_repository.dart b/lib/features/projects/domain/repositories/submissions_repository.dart index 1ab846a..5fdd785 100644 --- a/lib/features/projects/domain/repositories/submissions_repository.dart +++ b/lib/features/projects/domain/repositories/submissions_repository.dart @@ -38,6 +38,10 @@ abstract class SubmissionsRepository { int limitPageLength = 0, }); + /// Get project detail by name + /// Returns the full project detail as entity for form prefilling + Future getSubmissionDetail(String name); + /// Save (create/update) a project submission /// Returns the project name (ID) from the API response Future saveSubmission(ProjectSubmissionRequest request); diff --git a/lib/features/projects/presentation/pages/submission_create_page.dart b/lib/features/projects/presentation/pages/submission_create_page.dart index 1baa361..99f66bc 100644 --- a/lib/features/projects/presentation/pages/submission_create_page.dart +++ b/lib/features/projects/presentation/pages/submission_create_page.dart @@ -5,6 +5,7 @@ library; import 'dart:io'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -13,11 +14,17 @@ import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/theme/colors.dart'; import 'package:worker/features/projects/data/models/project_submission_request.dart'; import 'package:worker/features/projects/domain/entities/project_progress.dart'; +import 'package:worker/features/projects/domain/entities/project_submission.dart'; import 'package:worker/features/projects/presentation/providers/submissions_provider.dart'; -/// Project Submission Create Page +/// Project Submission Create/Edit Page class SubmissionCreatePage extends ConsumerStatefulWidget { - const SubmissionCreatePage({super.key}); + const SubmissionCreatePage({super.key, this.submission}); + + /// Optional submission for editing mode + /// If null, creates new submission + /// If provided, prefills form and updates existing submission + final ProjectSubmission? submission; @override ConsumerState createState() => @@ -40,8 +47,73 @@ class _SubmissionCreatePageState extends ConsumerState { // Form state ProjectProgress? _selectedProgress; DateTime? _expectedStartDate; - final List _uploadedFiles = []; + final List _uploadedFiles = []; // New files to upload + List _existingFiles = []; // Existing files from API bool _isSubmitting = false; + bool _isLoadingDetail = false; + + /// Whether we're editing an existing submission + bool get isEditing => widget.submission != null; + + @override + void initState() { + super.initState(); + // Fetch full detail when editing + if (isEditing) { + _loadDetail(); + } + } + + /// Load full project detail from API for editing + Future _loadDetail() async { + if (!isEditing) return; + + setState(() => _isLoadingDetail = true); + + try { + final detail = await ref.read( + submissionDetailProvider(widget.submission!.submissionId).future, + ); + + if (!mounted) return; + + // Prefill form fields from entity + _projectNameController.text = detail.designedArea; + _addressController.text = detail.addressOfProject ?? ''; + _ownerController.text = detail.projectOwner ?? ''; + _designUnitController.text = detail.designFirm ?? ''; + _constructionUnitController.text = detail.constructionContractor ?? ''; + _areaController.text = detail.designArea.toString(); + _productsController.text = detail.productsIncludedInTheDesign ?? ''; + _descriptionController.text = detail.description ?? ''; + + // Set expected commencement date + _expectedStartDate = detail.expectedCommencementDate; + + // Find matching progress from the list + final progressId = detail.projectProgress; + if (progressId != null) { + final progressList = await ref.read(projectProgressListProvider.future); + _selectedProgress = progressList.where((p) => p.id == progressId).firstOrNull; + } + + // Set existing files from API + _existingFiles = detail.filesList; + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Lỗi tải thông tin: $e'), + backgroundColor: AppColors.danger, + ), + ); + } + } finally { + if (mounted) { + setState(() => _isLoadingDetail = false); + } + } + } @override void dispose() { @@ -69,9 +141,9 @@ class _SubmissionCreatePageState extends ConsumerState { ), onPressed: () => Navigator.of(context).pop(), ), - title: const Text( - 'Đăng ký Công trình', - style: TextStyle(color: Colors.black), + title: Text( + isEditing ? 'Chỉnh sửa Dự án' : 'Đăng ký Công trình', + style: const TextStyle(color: Colors.black), ), actions: [ IconButton( @@ -88,7 +160,21 @@ class _SubmissionCreatePageState extends ConsumerState { backgroundColor: AppColors.white, centerTitle: false, ), - body: Form( + body: _isLoadingDetail + ? const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text( + 'Đang tải thông tin dự án...', + style: TextStyle(color: AppColors.grey500), + ), + ], + ), + ) + : Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(4), @@ -322,8 +408,41 @@ class _SubmissionCreatePageState extends ConsumerState { ), ), + // Existing files from API + if (_existingFiles.isNotEmpty) ...[ + const SizedBox(height: 16), + const Text( + 'Ảnh đã tải lên', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.grey500, + ), + ), + const SizedBox(height: 8), + ..._existingFiles.asMap().entries.map((entry) { + final index = entry.key; + final file = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildExistingFileItem(file, index), + ); + }), + ], + + // New files to upload if (_uploadedFiles.isNotEmpty) ...[ const SizedBox(height: 16), + if (_existingFiles.isNotEmpty) + const Text( + 'Ảnh mới', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.grey500, + ), + ), + if (_existingFiles.isNotEmpty) const SizedBox(height: 8), ..._uploadedFiles.asMap().entries.map((entry) { final index = entry.key; final file = entry.value; @@ -737,6 +856,88 @@ class _SubmissionCreatePageState extends ConsumerState { ); } + Widget _buildExistingFileItem(ProjectFile file, int index) { + final fileName = file.fileUrl.split('/').last; + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF8F9FA), + border: Border.all(color: AppColors.success), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + // Network image + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: CachedNetworkImage( + imageUrl: file.fileUrl, + width: 48, + height: 48, + fit: BoxFit.cover, + placeholder: (context, url) => Container( + width: 48, + height: 48, + color: AppColors.grey100, + child: const Center( + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ), + errorWidget: (context, url, error) => Container( + width: 48, + height: 48, + color: AppColors.grey100, + child: const Center( + child: FaIcon( + FontAwesomeIcons.image, + size: 24, + color: AppColors.grey500, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + fileName, + style: const TextStyle( + fontWeight: FontWeight.w500, + color: AppColors.grey900, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + const Text( + 'Đã tải lên', + style: TextStyle( + fontSize: 12, + color: AppColors.success, + ), + ), + ], + ), + ), + // Checkmark icon + const FaIcon( + FontAwesomeIcons.circleCheck, + size: 16, + color: AppColors.success, + ), + ], + ), + ); + } + Widget _buildSubmitButton() { return SizedBox( width: double.infinity, @@ -847,7 +1048,11 @@ class _SubmissionCreatePageState extends ConsumerState { context: context, builder: (context) => AlertDialog( title: const Text('Xác nhận'), - content: const Text('Xác nhận gửi đăng ký công trình?'), + content: Text( + isEditing + ? 'Xác nhận cập nhật thông tin dự án?' + : 'Xác nhận gửi đăng ký công trình?', + ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), @@ -870,7 +1075,9 @@ class _SubmissionCreatePageState extends ConsumerState { final area = double.tryParse(_areaController.text.trim()) ?? 0.0; // Create submission request + // Include name field when editing (for update) final request = ProjectSubmissionRequest( + name: isEditing ? widget.submission!.submissionId : null, designedArea: _projectNameController.text.trim(), addressOfProject: _addressController.text.trim(), projectOwner: _ownerController.text.trim(), @@ -907,9 +1114,11 @@ class _SubmissionCreatePageState extends ConsumerState { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( + SnackBar( content: Text( - 'Đăng ký công trình đã được gửi thành công!\nChúng tôi sẽ xem xét và liên hệ với bạn sớm nhất.', + isEditing + ? 'Cập nhật dự án thành công!' + : 'Đăng ký công trình đã được gửi thành công!\nChúng tôi sẽ xem xét và liên hệ với bạn sớm nhất.', ), backgroundColor: AppColors.success, ), diff --git a/lib/features/projects/presentation/pages/submissions_page.dart b/lib/features/projects/presentation/pages/submissions_page.dart index 4b4a142..3d16572 100644 --- a/lib/features/projects/presentation/pages/submissions_page.dart +++ b/lib/features/projects/presentation/pages/submissions_page.dart @@ -171,7 +171,7 @@ class SubmissionsPage extends ConsumerWidget { itemCount: submissions.length, itemBuilder: (context, index) { final submission = submissions[index]; - return _buildSubmissionCard(context, submission); + return _buildSubmissionCard(context, ref, submission); }, ), ); @@ -260,17 +260,22 @@ class SubmissionsPage extends ConsumerWidget { ); } - Widget _buildSubmissionCard(BuildContext context, ProjectSubmission submission) { + Widget _buildSubmissionCard(BuildContext context, WidgetRef ref, 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}')), + onTap: () async { + // Navigate to edit submission page + final result = await context.push( + RouteNames.submissionCreate, + extra: submission, ); + if (result == true) { + // Refresh submissions list after successful update + ref.invalidate(allSubmissionsProvider); + } }, borderRadius: BorderRadius.circular(12), child: Container( diff --git a/lib/features/projects/presentation/providers/submissions_provider.dart b/lib/features/projects/presentation/providers/submissions_provider.dart index 38a72c6..217f0af 100644 --- a/lib/features/projects/presentation/providers/submissions_provider.dart +++ b/lib/features/projects/presentation/providers/submissions_provider.dart @@ -193,6 +193,16 @@ AsyncValue> filteredSubmissions(Ref ref) { }); } +/// Submission Detail Provider +/// +/// Fetches full project detail by name for editing. +/// Uses family modifier to cache by submission name. +@riverpod +Future submissionDetail(Ref ref, String name) async { + final repository = await ref.watch(submissionsRepositoryProvider.future); + return repository.getSubmissionDetail(name); +} + /// Save Submission Provider /// /// Handles creating new project submissions via API. diff --git a/lib/features/projects/presentation/providers/submissions_provider.g.dart b/lib/features/projects/presentation/providers/submissions_provider.g.dart index 51b1a4e..08e1f80 100644 --- a/lib/features/projects/presentation/providers/submissions_provider.g.dart +++ b/lib/features/projects/presentation/providers/submissions_provider.g.dart @@ -569,6 +569,107 @@ final class FilteredSubmissionsProvider String _$filteredSubmissionsHash() => r'5be22b3242426c6b0c2f9778eaee5c7cf23e4814'; +/// Submission Detail Provider +/// +/// Fetches full project detail by name for editing. +/// Uses family modifier to cache by submission name. + +@ProviderFor(submissionDetail) +const submissionDetailProvider = SubmissionDetailFamily._(); + +/// Submission Detail Provider +/// +/// Fetches full project detail by name for editing. +/// Uses family modifier to cache by submission name. + +final class SubmissionDetailProvider + extends + $FunctionalProvider< + AsyncValue, + ProjectSubmission, + FutureOr + > + with + $FutureModifier, + $FutureProvider { + /// Submission Detail Provider + /// + /// Fetches full project detail by name for editing. + /// Uses family modifier to cache by submission name. + const SubmissionDetailProvider._({ + required SubmissionDetailFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'submissionDetailProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$submissionDetailHash(); + + @override + String toString() { + return r'submissionDetailProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as String; + return submissionDetail(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is SubmissionDetailProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$submissionDetailHash() => r'd3c767aa55e74a36c6a2b9b9bf6dd8ad8bf8eff3'; + +/// Submission Detail Provider +/// +/// Fetches full project detail by name for editing. +/// Uses family modifier to cache by submission name. + +final class SubmissionDetailFamily extends $Family + with $FunctionalFamilyOverride, String> { + const SubmissionDetailFamily._() + : super( + retry: null, + name: r'submissionDetailProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// Submission Detail Provider + /// + /// Fetches full project detail by name for editing. + /// Uses family modifier to cache by submission name. + + SubmissionDetailProvider call(String name) => + SubmissionDetailProvider._(argument: name, from: this); + + @override + String toString() => r'submissionDetailProvider'; +} + /// Save Submission Provider /// /// Handles creating new project submissions via API. diff --git a/lib/hive_registrar.g.dart b/lib/hive_registrar.g.dart index bd37a07..f3d4343 100644 --- a/lib/hive_registrar.g.dart +++ b/lib/hive_registrar.g.dart @@ -34,7 +34,6 @@ 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_progress_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'; import 'package:worker/features/showrooms/data/models/showroom_model.dart'; @@ -80,7 +79,6 @@ extension HiveRegistrar on HiveInterface { registerAdapter(ProductModelAdapter()); registerAdapter(ProjectProgressModelAdapter()); registerAdapter(ProjectStatusModelAdapter()); - registerAdapter(ProjectSubmissionModelAdapter()); registerAdapter(ProjectTypeAdapter()); registerAdapter(PromotionModelAdapter()); registerAdapter(QuoteItemModelAdapter()); @@ -141,7 +139,6 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface { registerAdapter(ProductModelAdapter()); registerAdapter(ProjectProgressModelAdapter()); registerAdapter(ProjectStatusModelAdapter()); - registerAdapter(ProjectSubmissionModelAdapter()); registerAdapter(ProjectTypeAdapter()); registerAdapter(PromotionModelAdapter()); registerAdapter(QuoteItemModelAdapter());