create submission
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user