submission
This commit is contained in:
@@ -139,3 +139,44 @@ curl --location 'https://land.dbiz.com//api/method/upload_file' \
|
|||||||
--form 'docname="p9ti8veq2g"' \
|
--form 'docname="p9ti8veq2g"' \
|
||||||
--form 'optimize="true"'
|
--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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -308,6 +308,13 @@ class ApiConstants {
|
|||||||
static const String saveProject =
|
static const String saveProject =
|
||||||
'/building_material.building_material.api.project.save';
|
'/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)
|
/// Create new project (legacy endpoint - may be deprecated)
|
||||||
/// POST /projects
|
/// POST /projects
|
||||||
static const String createProject = '/projects';
|
static const String createProject = '/projects';
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ final class LoggingInterceptorProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$loggingInterceptorHash() =>
|
String _$loggingInterceptorHash() =>
|
||||||
r'4d3147e9084d261e14653386ecd74ee471993af4';
|
r'6afa480caa6fcd723dab769bb01601b8a37e20fd';
|
||||||
|
|
||||||
/// Provider for ErrorTransformerInterceptor
|
/// Provider for ErrorTransformerInterceptor
|
||||||
|
|
||||||
|
|||||||
@@ -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/product_detail_page.dart';
|
||||||
import 'package:worker/features/products/presentation/pages/products_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/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/submission_create_page.dart';
|
||||||
import 'package:worker/features/projects/presentation/pages/submissions_page.dart';
|
import 'package:worker/features/projects/presentation/pages/submissions_page.dart';
|
||||||
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
|
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
|
||||||
@@ -391,12 +392,17 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
MaterialPage(key: state.pageKey, child: const SubmissionsPage()),
|
MaterialPage(key: state.pageKey, child: const SubmissionsPage()),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Submission Create Route
|
// Submission Create/Edit Route
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.submissionCreate,
|
path: RouteNames.submissionCreate,
|
||||||
name: RouteNames.submissionCreate,
|
name: RouteNames.submissionCreate,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) {
|
||||||
MaterialPage(key: state.pageKey, child: const SubmissionCreatePage()),
|
final submission = state.extra as ProjectSubmission?;
|
||||||
|
return MaterialPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: SubmissionCreatePage(submission: submission),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// Quotes Route
|
// Quotes Route
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ abstract class SubmissionsRemoteDataSource {
|
|||||||
int limitPageLength = 0,
|
int limitPageLength = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Fetch project detail by name
|
||||||
|
/// Returns the full project detail as a model
|
||||||
|
Future<ProjectSubmissionModel> getSubmissionDetail(String name);
|
||||||
|
|
||||||
/// Create or update a project submission
|
/// Create or update a project submission
|
||||||
/// Returns the project name (ID) from the API response
|
/// Returns the project name (ID) from the API response
|
||||||
Future<String> saveSubmission(ProjectSubmissionRequest request);
|
Future<String> 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<ProjectSubmissionModel> getSubmissionDetail(String name) async {
|
||||||
|
try {
|
||||||
|
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||||
|
'${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<String, dynamic>?;
|
||||||
|
if (message == null) {
|
||||||
|
throw Exception('No message field in getProjectDetail response');
|
||||||
|
}
|
||||||
|
|
||||||
|
final detailData = message['data'] as Map<String, dynamic>?;
|
||||||
|
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
|
/// Save (create/update) a project submission
|
||||||
///
|
///
|
||||||
/// Calls: POST /api/method/building_material.building_material.api.project.save
|
/// 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 fileName = filePath.split('/').last;
|
||||||
final formData = FormData.fromMap({
|
final formData = FormData.fromMap({
|
||||||
'file': await MultipartFile.fromFile(filePath, filename: fileName),
|
'file': await MultipartFile.fromFile(filePath, filename: fileName),
|
||||||
'is_private': '1',
|
'is_private': '0',
|
||||||
'folder': 'Home/Attachments',
|
'folder': 'Home/Attachments',
|
||||||
'doctype': 'Architectural Project',
|
'doctype': 'Architectural Project',
|
||||||
'docname': projectName,
|
'docname': projectName,
|
||||||
|
|||||||
@@ -1,47 +1,117 @@
|
|||||||
/// Project Submission Model
|
/// Project Submission Model
|
||||||
///
|
///
|
||||||
/// Data model for project submission from API responses with Hive caching.
|
/// Data model for project submission from API responses.
|
||||||
/// 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;
|
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';
|
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
|
/// Full URL to the file (API: file_url)
|
||||||
@HiveType(typeId: HiveTypeIds.projectSubmissionModel)
|
final String fileUrl;
|
||||||
class ProjectSubmissionModel extends HiveObject {
|
|
||||||
|
const ProjectFileModel({
|
||||||
|
required this.id,
|
||||||
|
required this.fileUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Create from JSON (API response)
|
||||||
|
factory ProjectFileModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ProjectFileModel(
|
||||||
|
id: json['name'] as String,
|
||||||
|
fileUrl: json['file_url'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert to JSON
|
||||||
|
Map<String, dynamic> 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)
|
/// Unique submission identifier (API: name)
|
||||||
@HiveField(0)
|
|
||||||
final String submissionId;
|
final String submissionId;
|
||||||
|
|
||||||
/// Project name/title (API: designed_area)
|
/// Project name/title (API: designed_area)
|
||||||
@HiveField(1)
|
|
||||||
final String designedArea;
|
final String designedArea;
|
||||||
|
|
||||||
/// Design area value in square meters (API: design_area)
|
/// Design area value in square meters (API: design_area)
|
||||||
@HiveField(2)
|
|
||||||
final double designArea;
|
final double designArea;
|
||||||
|
|
||||||
/// Submission/request date (API: request_date)
|
/// Submission/request date (API: request_date)
|
||||||
@HiveField(3)
|
|
||||||
final DateTime requestDate;
|
final DateTime requestDate;
|
||||||
|
|
||||||
/// Status label - Vietnamese (API: status)
|
/// Status label - Vietnamese (API: status)
|
||||||
@HiveField(4)
|
|
||||||
final String status;
|
final String status;
|
||||||
|
|
||||||
/// Rejection reason if rejected (API: reason_for_rejection)
|
/// Rejection reason if rejected (API: reason_for_rejection)
|
||||||
@HiveField(5)
|
|
||||||
final String? reasonForRejection;
|
final String? reasonForRejection;
|
||||||
|
|
||||||
/// Status color indicator (API: status_color)
|
/// Status color indicator (API: status_color)
|
||||||
@HiveField(6)
|
|
||||||
final String statusColor;
|
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<ProjectFileModel> filesList;
|
||||||
|
|
||||||
|
const ProjectSubmissionModel({
|
||||||
required this.submissionId,
|
required this.submissionId,
|
||||||
required this.designedArea,
|
required this.designedArea,
|
||||||
required this.designArea,
|
required this.designArea,
|
||||||
@@ -49,10 +119,39 @@ class ProjectSubmissionModel extends HiveObject {
|
|||||||
required this.status,
|
required this.status,
|
||||||
this.reasonForRejection,
|
this.reasonForRejection,
|
||||||
required this.statusColor,
|
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)
|
/// Create from JSON (API response)
|
||||||
|
/// Handles both list response and detail response formats
|
||||||
factory ProjectSubmissionModel.fromJson(Map<String, dynamic> json) {
|
factory ProjectSubmissionModel.fromJson(Map<String, dynamic> 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<dynamic>?;
|
||||||
|
final filesList = filesListJson
|
||||||
|
?.map((f) => ProjectFileModel.fromJson(f as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
return ProjectSubmissionModel(
|
return ProjectSubmissionModel(
|
||||||
submissionId: json['name'] as String,
|
submissionId: json['name'] as String,
|
||||||
designedArea: json['designed_area'] as String,
|
designedArea: json['designed_area'] as String,
|
||||||
@@ -61,6 +160,19 @@ class ProjectSubmissionModel extends HiveObject {
|
|||||||
status: json['status'] as String,
|
status: json['status'] as String,
|
||||||
reasonForRejection: json['reason_for_rejection'] as String?,
|
reasonForRejection: json['reason_for_rejection'] as String?,
|
||||||
statusColor: json['status_color'] 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,
|
'status': status,
|
||||||
'reason_for_rejection': reasonForRejection,
|
'reason_for_rejection': reasonForRejection,
|
||||||
'status_color': statusColor,
|
'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,
|
status: status,
|
||||||
reasonForRejection: reasonForRejection,
|
reasonForRejection: reasonForRejection,
|
||||||
statusColor: statusColor,
|
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,
|
status: entity.status,
|
||||||
reasonForRejection: entity.reasonForRejection,
|
reasonForRejection: entity.reasonForRejection,
|
||||||
statusColor: entity.statusColor,
|
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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'project_submission_model.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class ProjectSubmissionModelAdapter
|
|
||||||
extends TypeAdapter<ProjectSubmissionModel> {
|
|
||||||
@override
|
|
||||||
final typeId = 14;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ProjectSubmissionModel read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -138,6 +138,16 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ProjectSubmission> getSubmissionDetail(String name) async {
|
||||||
|
try {
|
||||||
|
final model = await _remoteDataSource.getSubmissionDetail(name);
|
||||||
|
return model.toEntity();
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> saveSubmission(ProjectSubmissionRequest request) async {
|
Future<String> saveSubmission(ProjectSubmissionRequest request) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,22 +1,53 @@
|
|||||||
/// Domain Entity: Project Submission
|
/// Domain Entity: Project Submission
|
||||||
///
|
///
|
||||||
/// Represents a completed project submitted for loyalty points.
|
/// 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;
|
library;
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
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<Object?> get props => [id, fileUrl];
|
||||||
|
}
|
||||||
|
|
||||||
/// Project Submission Entity
|
/// Project Submission Entity
|
||||||
///
|
///
|
||||||
/// Contains information about a completed project submission.
|
/// Contains information about a completed project submission.
|
||||||
/// Mapped from API response:
|
/// Mapped from API response:
|
||||||
/// - name -> submissionId
|
/// - name -> submissionId
|
||||||
/// - designed_area -> designedArea (project name/title)
|
/// - 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²)
|
/// - 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
|
/// - request_date -> requestDate
|
||||||
/// - status -> status (Vietnamese label)
|
/// - workflow_state -> workflowState
|
||||||
/// - reason_for_rejection -> reasonForRejection
|
/// - reason_for_rejection -> reasonForRejection
|
||||||
|
/// - status -> status (Vietnamese label)
|
||||||
/// - status_color -> statusColor
|
/// - status_color -> statusColor
|
||||||
|
/// - is_allow_modify -> isAllowModify
|
||||||
|
/// - is_allow_cancel -> isAllowCancel
|
||||||
|
/// - files_list -> filesList
|
||||||
class ProjectSubmission extends Equatable {
|
class ProjectSubmission extends Equatable {
|
||||||
/// Unique submission identifier (API: name)
|
/// Unique submission identifier (API: name)
|
||||||
final String submissionId;
|
final String submissionId;
|
||||||
@@ -24,31 +55,80 @@ class ProjectSubmission extends Equatable {
|
|||||||
/// Project name/title (API: designed_area)
|
/// Project name/title (API: designed_area)
|
||||||
final String designedArea;
|
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)
|
/// Design area value in square meters (API: design_area)
|
||||||
final double designArea;
|
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)
|
/// Submission/request date (API: request_date)
|
||||||
final DateTime requestDate;
|
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)
|
/// Status label - Vietnamese (API: status)
|
||||||
/// e.g., "Chờ phê duyệt", "Đã được phê duyệt", "Từ chối", "HỦY BỎ"
|
/// e.g., "Chờ phê duyệt", "Đã được phê duyệt", "Từ chối", "HỦY BỎ"
|
||||||
final String status;
|
final String status;
|
||||||
|
|
||||||
/// Rejection reason if rejected (API: reason_for_rejection)
|
|
||||||
final String? reasonForRejection;
|
|
||||||
|
|
||||||
/// Status color indicator (API: status_color)
|
/// Status color indicator (API: status_color)
|
||||||
/// Values: "Warning", "Success", "Danger"
|
/// Values: "Warning", "Success", "Danger"
|
||||||
final String statusColor;
|
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<ProjectFile> filesList;
|
||||||
|
|
||||||
const ProjectSubmission({
|
const ProjectSubmission({
|
||||||
required this.submissionId,
|
required this.submissionId,
|
||||||
required this.designedArea,
|
required this.designedArea,
|
||||||
|
this.addressOfProject,
|
||||||
|
this.projectOwner,
|
||||||
|
this.designFirm,
|
||||||
|
this.constructionContractor,
|
||||||
required this.designArea,
|
required this.designArea,
|
||||||
|
this.productsIncludedInTheDesign,
|
||||||
|
this.projectProgress,
|
||||||
|
this.expectedCommencementDate,
|
||||||
|
this.description,
|
||||||
required this.requestDate,
|
required this.requestDate,
|
||||||
required this.status,
|
this.workflowState,
|
||||||
this.reasonForRejection,
|
this.reasonForRejection,
|
||||||
|
required this.status,
|
||||||
required this.statusColor,
|
required this.statusColor,
|
||||||
|
this.isAllowModify = false,
|
||||||
|
this.isAllowCancel = false,
|
||||||
|
this.filesList = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Check if submission is pending approval
|
/// Check if submission is pending approval
|
||||||
@@ -64,20 +144,44 @@ class ProjectSubmission extends Equatable {
|
|||||||
ProjectSubmission copyWith({
|
ProjectSubmission copyWith({
|
||||||
String? submissionId,
|
String? submissionId,
|
||||||
String? designedArea,
|
String? designedArea,
|
||||||
|
String? addressOfProject,
|
||||||
|
String? projectOwner,
|
||||||
|
String? designFirm,
|
||||||
|
String? constructionContractor,
|
||||||
double? designArea,
|
double? designArea,
|
||||||
|
String? productsIncludedInTheDesign,
|
||||||
|
String? projectProgress,
|
||||||
|
DateTime? expectedCommencementDate,
|
||||||
|
String? description,
|
||||||
DateTime? requestDate,
|
DateTime? requestDate,
|
||||||
String? status,
|
String? workflowState,
|
||||||
String? reasonForRejection,
|
String? reasonForRejection,
|
||||||
|
String? status,
|
||||||
String? statusColor,
|
String? statusColor,
|
||||||
|
bool? isAllowModify,
|
||||||
|
bool? isAllowCancel,
|
||||||
|
List<ProjectFile>? filesList,
|
||||||
}) {
|
}) {
|
||||||
return ProjectSubmission(
|
return ProjectSubmission(
|
||||||
submissionId: submissionId ?? this.submissionId,
|
submissionId: submissionId ?? this.submissionId,
|
||||||
designedArea: designedArea ?? this.designedArea,
|
designedArea: designedArea ?? this.designedArea,
|
||||||
|
addressOfProject: addressOfProject ?? this.addressOfProject,
|
||||||
|
projectOwner: projectOwner ?? this.projectOwner,
|
||||||
|
designFirm: designFirm ?? this.designFirm,
|
||||||
|
constructionContractor: constructionContractor ?? this.constructionContractor,
|
||||||
designArea: designArea ?? this.designArea,
|
designArea: designArea ?? this.designArea,
|
||||||
|
productsIncludedInTheDesign: productsIncludedInTheDesign ?? this.productsIncludedInTheDesign,
|
||||||
|
projectProgress: projectProgress ?? this.projectProgress,
|
||||||
|
expectedCommencementDate: expectedCommencementDate ?? this.expectedCommencementDate,
|
||||||
|
description: description ?? this.description,
|
||||||
requestDate: requestDate ?? this.requestDate,
|
requestDate: requestDate ?? this.requestDate,
|
||||||
status: status ?? this.status,
|
workflowState: workflowState ?? this.workflowState,
|
||||||
reasonForRejection: reasonForRejection ?? this.reasonForRejection,
|
reasonForRejection: reasonForRejection ?? this.reasonForRejection,
|
||||||
|
status: status ?? this.status,
|
||||||
statusColor: statusColor ?? this.statusColor,
|
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<Object?> get props => [
|
List<Object?> get props => [
|
||||||
submissionId,
|
submissionId,
|
||||||
designedArea,
|
designedArea,
|
||||||
|
addressOfProject,
|
||||||
|
projectOwner,
|
||||||
|
designFirm,
|
||||||
|
constructionContractor,
|
||||||
designArea,
|
designArea,
|
||||||
|
productsIncludedInTheDesign,
|
||||||
|
projectProgress,
|
||||||
|
expectedCommencementDate,
|
||||||
|
description,
|
||||||
requestDate,
|
requestDate,
|
||||||
status,
|
workflowState,
|
||||||
reasonForRejection,
|
reasonForRejection,
|
||||||
|
status,
|
||||||
statusColor,
|
statusColor,
|
||||||
|
isAllowModify,
|
||||||
|
isAllowCancel,
|
||||||
|
filesList,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ abstract class SubmissionsRepository {
|
|||||||
int limitPageLength = 0,
|
int limitPageLength = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Get project detail by name
|
||||||
|
/// Returns the full project detail as entity for form prefilling
|
||||||
|
Future<ProjectSubmission> getSubmissionDetail(String name);
|
||||||
|
|
||||||
/// Save (create/update) a project submission
|
/// Save (create/update) a project submission
|
||||||
/// Returns the project name (ID) from the API response
|
/// Returns the project name (ID) from the API response
|
||||||
Future<String> saveSubmission(ProjectSubmissionRequest request);
|
Future<String> saveSubmission(ProjectSubmissionRequest request);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ library;
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/core/theme/colors.dart';
|
||||||
import 'package:worker/features/projects/data/models/project_submission_request.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_progress.dart';
|
||||||
|
import 'package:worker/features/projects/domain/entities/project_submission.dart';
|
||||||
import 'package:worker/features/projects/presentation/providers/submissions_provider.dart';
|
import 'package:worker/features/projects/presentation/providers/submissions_provider.dart';
|
||||||
|
|
||||||
/// Project Submission Create Page
|
/// Project Submission Create/Edit Page
|
||||||
class SubmissionCreatePage extends ConsumerStatefulWidget {
|
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
|
@override
|
||||||
ConsumerState<SubmissionCreatePage> createState() =>
|
ConsumerState<SubmissionCreatePage> createState() =>
|
||||||
@@ -40,8 +47,73 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
// Form state
|
// Form state
|
||||||
ProjectProgress? _selectedProgress;
|
ProjectProgress? _selectedProgress;
|
||||||
DateTime? _expectedStartDate;
|
DateTime? _expectedStartDate;
|
||||||
final List<File> _uploadedFiles = [];
|
final List<File> _uploadedFiles = []; // New files to upload
|
||||||
|
List<ProjectFile> _existingFiles = []; // Existing files from API
|
||||||
bool _isSubmitting = false;
|
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<void> _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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -69,9 +141,9 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
),
|
),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Đăng ký Công trình',
|
isEditing ? 'Chỉnh sửa Dự án' : 'Đăng ký Công trình',
|
||||||
style: TextStyle(color: Colors.black),
|
style: const TextStyle(color: Colors.black),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -88,7 +160,21 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
backgroundColor: AppColors.white,
|
backgroundColor: AppColors.white,
|
||||||
centerTitle: false,
|
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,
|
key: _formKey,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
@@ -322,8 +408,41 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 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) ...[
|
if (_uploadedFiles.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
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) {
|
..._uploadedFiles.asMap().entries.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
final file = entry.value;
|
final file = entry.value;
|
||||||
@@ -737,6 +856,88 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
Widget _buildSubmitButton() {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -847,7 +1048,11 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Xác nhận'),
|
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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, false),
|
onPressed: () => Navigator.pop(context, false),
|
||||||
@@ -870,7 +1075,9 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
final area = double.tryParse(_areaController.text.trim()) ?? 0.0;
|
final area = double.tryParse(_areaController.text.trim()) ?? 0.0;
|
||||||
|
|
||||||
// Create submission request
|
// Create submission request
|
||||||
|
// Include name field when editing (for update)
|
||||||
final request = ProjectSubmissionRequest(
|
final request = ProjectSubmissionRequest(
|
||||||
|
name: isEditing ? widget.submission!.submissionId : null,
|
||||||
designedArea: _projectNameController.text.trim(),
|
designedArea: _projectNameController.text.trim(),
|
||||||
addressOfProject: _addressController.text.trim(),
|
addressOfProject: _addressController.text.trim(),
|
||||||
projectOwner: _ownerController.text.trim(),
|
projectOwner: _ownerController.text.trim(),
|
||||||
@@ -907,9 +1114,11 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
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,
|
backgroundColor: AppColors.success,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ class SubmissionsPage extends ConsumerWidget {
|
|||||||
itemCount: submissions.length,
|
itemCount: submissions.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final submission = submissions[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(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
// TODO: Navigate to submission detail
|
// Navigate to edit submission page
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
final result = await context.push<bool>(
|
||||||
SnackBar(content: Text('Chi tiết dự án ${submission.submissionId}')),
|
RouteNames.submissionCreate,
|
||||||
|
extra: submission,
|
||||||
);
|
);
|
||||||
|
if (result == true) {
|
||||||
|
// Refresh submissions list after successful update
|
||||||
|
ref.invalidate(allSubmissionsProvider);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|||||||
@@ -193,6 +193,16 @@ AsyncValue<List<ProjectSubmission>> filteredSubmissions(Ref ref) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Submission Detail Provider
|
||||||
|
///
|
||||||
|
/// Fetches full project detail by name for editing.
|
||||||
|
/// Uses family modifier to cache by submission name.
|
||||||
|
@riverpod
|
||||||
|
Future<ProjectSubmission> submissionDetail(Ref ref, String name) async {
|
||||||
|
final repository = await ref.watch(submissionsRepositoryProvider.future);
|
||||||
|
return repository.getSubmissionDetail(name);
|
||||||
|
}
|
||||||
|
|
||||||
/// Save Submission Provider
|
/// Save Submission Provider
|
||||||
///
|
///
|
||||||
/// Handles creating new project submissions via API.
|
/// Handles creating new project submissions via API.
|
||||||
|
|||||||
@@ -569,6 +569,107 @@ final class FilteredSubmissionsProvider
|
|||||||
String _$filteredSubmissionsHash() =>
|
String _$filteredSubmissionsHash() =>
|
||||||
r'5be22b3242426c6b0c2f9778eaee5c7cf23e4814';
|
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>,
|
||||||
|
ProjectSubmission,
|
||||||
|
FutureOr<ProjectSubmission>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<ProjectSubmission>,
|
||||||
|
$FutureProvider<ProjectSubmission> {
|
||||||
|
/// 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<ProjectSubmission> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<ProjectSubmission> 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<FutureOr<ProjectSubmission>, 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
|
/// Save Submission Provider
|
||||||
///
|
///
|
||||||
/// Handles creating new project submissions via API.
|
/// Handles creating new project submissions via API.
|
||||||
|
|||||||
@@ -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/design_request_model.dart';
|
||||||
import 'package:worker/features/projects/data/models/project_progress_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_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_item_model.dart';
|
||||||
import 'package:worker/features/quotes/data/models/quote_model.dart';
|
import 'package:worker/features/quotes/data/models/quote_model.dart';
|
||||||
import 'package:worker/features/showrooms/data/models/showroom_model.dart';
|
import 'package:worker/features/showrooms/data/models/showroom_model.dart';
|
||||||
@@ -80,7 +79,6 @@ extension HiveRegistrar on HiveInterface {
|
|||||||
registerAdapter(ProductModelAdapter());
|
registerAdapter(ProductModelAdapter());
|
||||||
registerAdapter(ProjectProgressModelAdapter());
|
registerAdapter(ProjectProgressModelAdapter());
|
||||||
registerAdapter(ProjectStatusModelAdapter());
|
registerAdapter(ProjectStatusModelAdapter());
|
||||||
registerAdapter(ProjectSubmissionModelAdapter());
|
|
||||||
registerAdapter(ProjectTypeAdapter());
|
registerAdapter(ProjectTypeAdapter());
|
||||||
registerAdapter(PromotionModelAdapter());
|
registerAdapter(PromotionModelAdapter());
|
||||||
registerAdapter(QuoteItemModelAdapter());
|
registerAdapter(QuoteItemModelAdapter());
|
||||||
@@ -141,7 +139,6 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
|||||||
registerAdapter(ProductModelAdapter());
|
registerAdapter(ProductModelAdapter());
|
||||||
registerAdapter(ProjectProgressModelAdapter());
|
registerAdapter(ProjectProgressModelAdapter());
|
||||||
registerAdapter(ProjectStatusModelAdapter());
|
registerAdapter(ProjectStatusModelAdapter());
|
||||||
registerAdapter(ProjectSubmissionModelAdapter());
|
|
||||||
registerAdapter(ProjectTypeAdapter());
|
registerAdapter(ProjectTypeAdapter());
|
||||||
registerAdapter(PromotionModelAdapter());
|
registerAdapter(PromotionModelAdapter());
|
||||||
registerAdapter(QuoteItemModelAdapter());
|
registerAdapter(QuoteItemModelAdapter());
|
||||||
|
|||||||
Reference in New Issue
Block a user