create submission

This commit is contained in:
Phuoc Nguyen
2025-11-27 16:56:01 +07:00
parent ba04576750
commit b6cb9e865a
18 changed files with 1445 additions and 138 deletions

View File

@@ -0,0 +1,45 @@
/// Project Progress Local Data Source
///
/// Handles local caching of project progress list using Hive.
library;
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/features/projects/data/models/project_progress_model.dart';
/// Project Progress Local Data Source
class ProjectProgressLocalDataSource {
/// Get Hive box for project progress
Box<dynamic> get _box => Hive.box(HiveBoxNames.projectProgressBox);
/// Save project progress list to cache
Future<void> cacheProgressList(List<ProjectProgressModel> progressList) async {
// Clear existing cache
await _box.clear();
// Save each progress with its id as key
for (final progress in progressList) {
await _box.put(progress.id, progress);
}
}
/// Get cached project progress list
List<ProjectProgressModel> getCachedProgressList() {
try {
final values = _box.values.whereType<ProjectProgressModel>().toList();
return values;
} catch (e) {
return [];
}
}
/// Check if cache exists and is not empty
bool hasCachedData() {
return _box.isNotEmpty;
}
/// Clear all cached progress
Future<void> clearCache() async {
await _box.clear();
}
}

View File

@@ -3,10 +3,13 @@
/// Handles remote API calls for project submissions.
library;
import 'package:dio/dio.dart';
import 'package:worker/core/constants/api_constants.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/features/projects/data/models/project_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/projects/data/models/project_submission_request.dart';
/// Submissions Remote Data Source
///
@@ -15,11 +18,27 @@ abstract class SubmissionsRemoteDataSource {
/// Fetch project status list from API
Future<List<ProjectStatusModel>> getProjectStatusList();
/// Fetch project progress list from API (construction stages)
Future<List<ProjectProgressModel>> getProjectProgressList();
/// Fetch all submissions from remote API
Future<List<ProjectSubmissionModel>> getSubmissions({
int limitStart = 0,
int limitPageLength = 0,
});
/// Create or update a project submission
/// Returns the project name (ID) from the API response
Future<String> saveSubmission(ProjectSubmissionRequest request);
/// Upload a file for a project submission
/// [projectName] is the project ID returned from saveSubmission
/// [filePath] is the local path to the file
/// Returns the uploaded file URL
Future<String> uploadProjectFile({
required String projectName,
required String filePath,
});
}
/// Submissions Remote Data Source Implementation
@@ -67,6 +86,50 @@ class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource {
}
}
/// Get project progress list (construction stages)
///
/// Calls: POST /api/method/frappe.client.get_list
/// Body: {
/// "doctype": "Progress of construction",
/// "fields": ["name", "status"],
/// "order_by": "number_of_display asc",
/// "limit_page_length": 0
/// }
/// Returns: List of construction progress stages
@override
Future<List<ProjectProgressModel>> getProjectProgressList() async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.frappeGetList}',
data: {
'doctype': 'Progress of construction',
'fields': ['name', 'status'],
'order_by': 'number_of_display asc',
'limit_page_length': 0,
},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getProjectProgressList API');
}
// API returns: { "message": [...] }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getProjectProgressList response');
}
final List<dynamic> progressList = message as List<dynamic>;
return progressList
.map((json) =>
ProjectProgressModel.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
throw Exception('Failed to get project progress list: $e');
}
}
/// Get list of project submissions
///
/// Calls: POST /api/method/building_material.building_material.api.project.get_list
@@ -106,4 +169,98 @@ class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource {
throw Exception('Failed to get project submissions: $e');
}
}
/// Save (create/update) a project submission
///
/// Calls: POST /api/method/building_material.building_material.api.project.save
/// Body: ProjectSubmissionRequest.toJson()
/// Returns: Project name (ID) from response
@override
Future<String> saveSubmission(ProjectSubmissionRequest request) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.saveProject}',
data: request.toJson(),
);
final data = response.data;
if (data == null) {
throw Exception('No data received from saveProject API');
}
// Check for error in response
if (data['exc_type'] != null || data['exception'] != null) {
final errorMessage =
data['_server_messages'] ?? data['exception'] ?? 'Unknown error';
throw Exception('API error: $errorMessage');
}
// Extract project name from response
// Response format: { "message": { "success": true, "data": { "name": "#DA00007" } } }
final message = data['message'] as Map<String, dynamic>?;
if (message == null) {
throw Exception('No message in saveProject response');
}
final messageData = message['data'] as Map<String, dynamic>?;
if (messageData == null || messageData['name'] == null) {
throw Exception('No project name in saveProject response');
}
return messageData['name'] as String;
} catch (e) {
throw Exception('Failed to save project submission: $e');
}
}
/// Upload a file for a project submission
///
/// Calls: POST /api/method/upload_file
/// Form-data: file, is_private, folder, doctype, docname, optimize
/// Returns: Uploaded file URL
@override
Future<String> uploadProjectFile({
required String projectName,
required String filePath,
}) async {
try {
final fileName = filePath.split('/').last;
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(filePath, filename: fileName),
'is_private': '1',
'folder': 'Home/Attachments',
'doctype': 'Architectural Project',
'docname': projectName,
'optimize': 'true',
});
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.uploadFile}',
data: formData,
);
final data = response.data;
if (data == null) {
throw Exception('No data received from uploadFile API');
}
// Check for error in response
if (data['exc_type'] != null || data['exception'] != null) {
final errorMessage =
data['_server_messages'] ?? data['exception'] ?? 'Unknown error';
throw Exception('API error: $errorMessage');
}
// Extract file URL from response
// Response format: { "message": { "file_url": "/files/...", ... } }
final message = data['message'];
if (message == null || message['file_url'] == null) {
throw Exception('No file URL in uploadFile response');
}
return message['file_url'] as String;
} catch (e) {
throw Exception('Failed to upload project file: $e');
}
}
}

View File

@@ -0,0 +1,60 @@
/// Project Progress Model
///
/// Data model for project progress from API responses with Hive caching.
/// Based on API response from frappe.client.get_list with doctype "Progress of construction"
library;
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/features/projects/domain/entities/project_progress.dart';
part 'project_progress_model.g.dart';
/// Project Progress Model - Type ID: 64
@HiveType(typeId: HiveTypeIds.projectProgressModel)
class ProjectProgressModel extends HiveObject {
/// Unique identifier (API: name)
@HiveField(0)
final String id;
/// Progress status label in Vietnamese (API: status)
@HiveField(1)
final String status;
ProjectProgressModel({
required this.id,
required this.status,
});
/// Create from JSON (API response)
factory ProjectProgressModel.fromJson(Map<String, dynamic> json) {
return ProjectProgressModel(
id: json['name'] as String,
status: json['status'] as String,
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'name': id,
'status': status,
};
}
/// Convert to entity
ProjectProgress toEntity() {
return ProjectProgress(
id: id,
status: status,
);
}
/// Create from entity
factory ProjectProgressModel.fromEntity(ProjectProgress entity) {
return ProjectProgressModel(
id: entity.id,
status: entity.status,
);
}
}

View File

@@ -0,0 +1,44 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'project_progress_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ProjectProgressModelAdapter extends TypeAdapter<ProjectProgressModel> {
@override
final typeId = 64;
@override
ProjectProgressModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ProjectProgressModel(
id: fields[0] as String,
status: fields[1] as String,
);
}
@override
void write(BinaryWriter writer, ProjectProgressModel obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.status);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ProjectProgressModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,119 @@
/// Project Submission Request Model
///
/// Request model for creating/updating project submissions via API.
/// Based on API: building_material.building_material.api.project.save
library;
import 'package:intl/intl.dart';
/// Project Submission Request
///
/// Used to create or update project submissions.
class ProjectSubmissionRequest {
/// Project ID (optional for new, required for update)
final String? name;
/// 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? contractionContractor;
/// Design area in m² (API: design_area)
final double designArea;
/// Products included in the design (API: products_included_in_the_design)
final String productsIncludedInTheDesign;
/// Project progress ID from ProjectProgress.id (API: project_progress)
final String projectProgress;
/// Expected commencement date (API: expected_commencement_date)
final DateTime? expectedCommencementDate;
/// Project description (API: description)
final String? description;
/// Request date (API: request_date)
final DateTime? requestDate;
const ProjectSubmissionRequest({
this.name,
required this.designedArea,
required this.addressOfProject,
required this.projectOwner,
this.designFirm,
this.contractionContractor,
required this.designArea,
required this.productsIncludedInTheDesign,
required this.projectProgress,
this.expectedCommencementDate,
this.description,
this.requestDate,
});
/// Convert to JSON for API request
Map<String, dynamic> toJson() {
final dateFormat = DateFormat('yyyy-MM-dd');
final dateTimeFormat = DateFormat('yyyy-MM-dd HH:mm:ss');
return {
if (name != null) 'name': name,
'designed_area': designedArea,
'address_of_project': addressOfProject,
'project_owner': projectOwner,
if (designFirm != null) 'design_firm': designFirm,
if (contractionContractor != null)
'contruction_contractor': contractionContractor,
'design_area': designArea,
'products_included_in_the_design': productsIncludedInTheDesign,
'project_progress': projectProgress,
if (expectedCommencementDate != null)
'expected_commencement_date': dateFormat.format(expectedCommencementDate!),
if (description != null) 'description': description,
'request_date': dateTimeFormat.format(requestDate ?? DateTime.now()),
};
}
/// Create a copy with updated fields
ProjectSubmissionRequest copyWith({
String? name,
String? designedArea,
String? addressOfProject,
String? projectOwner,
String? designFirm,
String? contractionContractor,
double? designArea,
String? productsIncludedInTheDesign,
String? projectProgress,
DateTime? expectedCommencementDate,
String? description,
DateTime? requestDate,
}) {
return ProjectSubmissionRequest(
name: name ?? this.name,
designedArea: designedArea ?? this.designedArea,
addressOfProject: addressOfProject ?? this.addressOfProject,
projectOwner: projectOwner ?? this.projectOwner,
designFirm: designFirm ?? this.designFirm,
contractionContractor: contractionContractor ?? this.contractionContractor,
designArea: designArea ?? this.designArea,
productsIncludedInTheDesign:
productsIncludedInTheDesign ?? this.productsIncludedInTheDesign,
projectProgress: projectProgress ?? this.projectProgress,
expectedCommencementDate:
expectedCommencementDate ?? this.expectedCommencementDate,
description: description ?? this.description,
requestDate: requestDate ?? this.requestDate,
);
}
}

View File

@@ -3,8 +3,11 @@
/// Implements the submissions repository interface with caching support.
library;
import 'package:worker/features/projects/data/datasources/project_progress_local_datasource.dart';
import 'package:worker/features/projects/data/datasources/project_status_local_datasource.dart';
import 'package:worker/features/projects/data/datasources/submissions_remote_datasource.dart';
import 'package:worker/features/projects/data/models/project_submission_request.dart';
import 'package:worker/features/projects/domain/entities/project_progress.dart';
import 'package:worker/features/projects/domain/entities/project_status.dart';
import 'package:worker/features/projects/domain/entities/project_submission.dart';
import 'package:worker/features/projects/domain/repositories/submissions_repository.dart';
@@ -16,10 +19,12 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
const SubmissionsRepositoryImpl(
this._remoteDataSource,
this._statusLocalDataSource,
this._progressLocalDataSource,
);
final SubmissionsRemoteDataSource _remoteDataSource;
final ProjectStatusLocalDataSource _statusLocalDataSource;
final ProjectProgressLocalDataSource _progressLocalDataSource;
/// Get project status list with cache-first pattern
///
@@ -69,6 +74,54 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
}
}
/// Get project progress list with cache-first pattern
///
/// 1. Return cached data if available
/// 2. Fetch from API in background and update cache
/// 3. If no cache, wait for API response
@override
Future<List<ProjectProgress>> getProjectProgressList({
bool forceRefresh = false,
}) async {
// Check cache first (unless force refresh)
if (!forceRefresh && _progressLocalDataSource.hasCachedData()) {
final cachedProgress = _progressLocalDataSource.getCachedProgressList();
if (cachedProgress.isNotEmpty) {
// Return cached data immediately
// Also refresh cache in background (fire and forget)
_refreshProgressCache();
return cachedProgress.map((model) => model.toEntity()).toList();
}
}
// No cache or force refresh - fetch from API
try {
final progressModels = await _remoteDataSource.getProjectProgressList();
// Cache the result
await _progressLocalDataSource.cacheProgressList(progressModels);
return progressModels.map((model) => model.toEntity()).toList();
} catch (e) {
// If API fails, try to return cached data as fallback
final cachedProgress = _progressLocalDataSource.getCachedProgressList();
if (cachedProgress.isNotEmpty) {
return cachedProgress.map((model) => model.toEntity()).toList();
}
rethrow;
}
}
/// Refresh progress cache in background
Future<void> _refreshProgressCache() async {
try {
final progressModels = await _remoteDataSource.getProjectProgressList();
await _progressLocalDataSource.cacheProgressList(progressModels);
} catch (e) {
// Silently fail - we already returned cached data
}
}
@override
Future<List<ProjectSubmission>> getSubmissions({
int limitStart = 0,
@@ -84,4 +137,28 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
rethrow;
}
}
@override
Future<String> saveSubmission(ProjectSubmissionRequest request) async {
try {
return await _remoteDataSource.saveSubmission(request);
} catch (e) {
rethrow;
}
}
@override
Future<String> uploadProjectFile({
required String projectName,
required String filePath,
}) async {
try {
return await _remoteDataSource.uploadProjectFile(
projectName: projectName,
filePath: filePath,
);
} catch (e) {
rethrow;
}
}
}