submission

This commit is contained in:
Phuoc Nguyen
2025-11-27 17:58:13 +07:00
parent b6cb9e865a
commit 6e7e848ad6
15 changed files with 745 additions and 109 deletions

View File

@@ -27,6 +27,10 @@ abstract class SubmissionsRemoteDataSource {
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
/// Returns the project name (ID) from the API response
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
///
/// 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,

View File

@@ -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<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)
@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<ProjectFileModel> 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<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(
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(),
);
}
}

View File

@@ -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;
}

View File

@@ -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
Future<String> saveSubmission(ProjectSubmissionRequest request) async {
try {