create submission
This commit is contained in:
@@ -62,3 +62,80 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#get project progress
|
||||||
|
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
||||||
|
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||||
|
--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 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"doctype": "Progress of construction",
|
||||||
|
"fields": ["name","status"],
|
||||||
|
"order_by": "number_of_display asc",
|
||||||
|
"limit_page_length": 0
|
||||||
|
}'
|
||||||
|
|
||||||
|
#response
|
||||||
|
{
|
||||||
|
"message": [
|
||||||
|
{
|
||||||
|
"name": "h6n0hat3o2",
|
||||||
|
"status": "Chưa khởi công"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "k1mr565o91",
|
||||||
|
"status": "Khởi công móng"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "2obpqokr8q",
|
||||||
|
"status": "Đang phần thô"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "i5qkovb09j",
|
||||||
|
"status": "Đang hoàn thiện"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kdj1jjlr28",
|
||||||
|
"status": "Cất nóc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "254e3ealdf",
|
||||||
|
"status": "Hoàn thiện"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#create new project
|
||||||
|
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.save' \
|
||||||
|
--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": "p9ti8veq2g",
|
||||||
|
"designed_area": "Sunrise Villa Phase 355",
|
||||||
|
"address_of_project": "123 Đường Võ Văn Kiệt, Quận 2, TP.HCM",
|
||||||
|
"project_owner": "Nguyễn Văn A",
|
||||||
|
"design_firm": "Studio Green",
|
||||||
|
"contruction_contractor": "CTCP Xây Dựng Minh Phú",
|
||||||
|
"design_area": 350.5,
|
||||||
|
"products_included_in_the_design": "Gạch ốp lát, sơn ngoại thất, \nkhóa thông minh",
|
||||||
|
"project_progress": "h6n0hat3o2",
|
||||||
|
"expected_commencement_date": "2026-01-15",
|
||||||
|
"description": "Yêu cầu phối màu mới cho khu vực hồ bơi",
|
||||||
|
"request_date": "2025-11-26 09:30:00"
|
||||||
|
}'
|
||||||
|
|
||||||
|
#upload image file for project
|
||||||
|
#docname is the project name returned from create new project
|
||||||
|
#file is the local path of the file to be uploaded
|
||||||
|
#other parameters can be kept as is
|
||||||
|
curl --location 'https://land.dbiz.com//api/method/upload_file' \
|
||||||
|
--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' \
|
||||||
|
--form 'file=@"/C:/Users/tiennld/Downloads/76369094c7604b3e1271.jpg"' \
|
||||||
|
--form 'is_private="1"' \
|
||||||
|
--form 'folder="Home/Attachments"' \
|
||||||
|
--form 'doctype="Architectural Project"' \
|
||||||
|
--form 'docname="p9ti8veq2g"' \
|
||||||
|
--form 'optimize="true"'
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,25 @@ class ApiConstants {
|
|||||||
static const String getProjectList =
|
static const String getProjectList =
|
||||||
'/building_material.building_material.api.project.get_list';
|
'/building_material.building_material.api.project.get_list';
|
||||||
|
|
||||||
|
/// Save (create/update) project submission
|
||||||
|
/// POST /api/method/building_material.building_material.api.project.save
|
||||||
|
/// Body: {
|
||||||
|
/// "name": "...", // optional for new, required for update
|
||||||
|
/// "designed_area": "Project Name",
|
||||||
|
/// "address_of_project": "...",
|
||||||
|
/// "project_owner": "...",
|
||||||
|
/// "design_firm": "...",
|
||||||
|
/// "contruction_contractor": "...",
|
||||||
|
/// "design_area": 350.5,
|
||||||
|
/// "products_included_in_the_design": "...",
|
||||||
|
/// "project_progress": "progress_id", // from ProjectProgress.id
|
||||||
|
/// "expected_commencement_date": "2026-01-15",
|
||||||
|
/// "description": "...",
|
||||||
|
/// "request_date": "2025-11-26 09:30:00"
|
||||||
|
/// }
|
||||||
|
static const String saveProject =
|
||||||
|
'/building_material.building_material.api.project.save';
|
||||||
|
|
||||||
/// 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';
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ class HiveBoxNames {
|
|||||||
/// Project status list cache
|
/// Project status list cache
|
||||||
static const String projectStatusBox = 'project_status_box';
|
static const String projectStatusBox = 'project_status_box';
|
||||||
|
|
||||||
|
/// Project progress list cache (construction stages)
|
||||||
|
static const String projectProgressBox = 'project_progress_box';
|
||||||
|
|
||||||
/// Get all box names for initialization
|
/// Get all box names for initialization
|
||||||
static List<String> get allBoxes => [
|
static List<String> get allBoxes => [
|
||||||
userBox,
|
userBox,
|
||||||
@@ -81,6 +84,7 @@ class HiveBoxNames {
|
|||||||
wardBox,
|
wardBox,
|
||||||
orderStatusBox,
|
orderStatusBox,
|
||||||
projectStatusBox,
|
projectStatusBox,
|
||||||
|
projectProgressBox,
|
||||||
settingsBox,
|
settingsBox,
|
||||||
cacheBox,
|
cacheBox,
|
||||||
syncStateBox,
|
syncStateBox,
|
||||||
@@ -144,6 +148,7 @@ class HiveTypeIds {
|
|||||||
static const int wardModel = 32;
|
static const int wardModel = 32;
|
||||||
static const int orderStatusModel = 62;
|
static const int orderStatusModel = 62;
|
||||||
static const int projectStatusModel = 63;
|
static const int projectStatusModel = 63;
|
||||||
|
static const int projectProgressModel = 64;
|
||||||
|
|
||||||
// Enums (33-61)
|
// Enums (33-61)
|
||||||
static const int userRole = 33;
|
static const int userRole = 33;
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ class HiveService {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectStatusModel) ? "✓" : "✗"} ProjectStatusModel adapter',
|
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectStatusModel) ? "✓" : "✗"} ProjectStatusModel adapter',
|
||||||
);
|
);
|
||||||
|
debugPrint(
|
||||||
|
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectProgressModel) ? "✓" : "✗"} ProjectProgressModel adapter',
|
||||||
|
);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.entryType) ? "✓" : "✗"} EntryType adapter',
|
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.entryType) ? "✓" : "✗"} EntryType adapter',
|
||||||
);
|
);
|
||||||
@@ -180,6 +183,9 @@ class HiveService {
|
|||||||
|
|
||||||
// Project status box (non-sensitive) - caches project status list from API
|
// Project status box (non-sensitive) - caches project status list from API
|
||||||
Hive.openBox<dynamic>(HiveBoxNames.projectStatusBox),
|
Hive.openBox<dynamic>(HiveBoxNames.projectStatusBox),
|
||||||
|
|
||||||
|
// Project progress box (non-sensitive) - caches construction progress stages from API
|
||||||
|
Hive.openBox<dynamic>(HiveBoxNames.projectProgressBox),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Open potentially encrypted boxes (sensitive data)
|
// Open potentially encrypted boxes (sensitive data)
|
||||||
|
|||||||
@@ -569,7 +569,7 @@ Future<AuthInterceptor> authInterceptor(Ref ref, Dio dio) async {
|
|||||||
@riverpod
|
@riverpod
|
||||||
LoggingInterceptor loggingInterceptor(Ref ref) {
|
LoggingInterceptor loggingInterceptor(Ref ref) {
|
||||||
// Only enable logging in debug mode
|
// Only enable logging in debug mode
|
||||||
const bool isDebug = false; // TODO: Replace with kDebugMode from Flutter
|
const bool isDebug = true; // TODO: Replace with kDebugMode from Flutter
|
||||||
|
|
||||||
return LoggingInterceptor(
|
return LoggingInterceptor(
|
||||||
enableRequestLogging: false,
|
enableRequestLogging: false,
|
||||||
|
|||||||
@@ -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.
|
/// Handles remote API calls for project submissions.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:worker/core/constants/api_constants.dart';
|
import 'package:worker/core/constants/api_constants.dart';
|
||||||
import 'package:worker/core/network/dio_client.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_status_model.dart';
|
||||||
import 'package:worker/features/projects/data/models/project_submission_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
|
/// Submissions Remote Data Source
|
||||||
///
|
///
|
||||||
@@ -15,11 +18,27 @@ abstract class SubmissionsRemoteDataSource {
|
|||||||
/// Fetch project status list from API
|
/// Fetch project status list from API
|
||||||
Future<List<ProjectStatusModel>> getProjectStatusList();
|
Future<List<ProjectStatusModel>> getProjectStatusList();
|
||||||
|
|
||||||
|
/// Fetch project progress list from API (construction stages)
|
||||||
|
Future<List<ProjectProgressModel>> getProjectProgressList();
|
||||||
|
|
||||||
/// Fetch all submissions from remote API
|
/// Fetch all submissions from remote API
|
||||||
Future<List<ProjectSubmissionModel>> getSubmissions({
|
Future<List<ProjectSubmissionModel>> getSubmissions({
|
||||||
int limitStart = 0,
|
int limitStart = 0,
|
||||||
int limitPageLength = 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
|
/// 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
|
/// Get list of project submissions
|
||||||
///
|
///
|
||||||
/// Calls: POST /api/method/building_material.building_material.api.project.get_list
|
/// 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');
|
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.
|
/// Implements the submissions repository interface with caching support.
|
||||||
library;
|
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/project_status_local_datasource.dart';
|
||||||
import 'package:worker/features/projects/data/datasources/submissions_remote_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_status.dart';
|
||||||
import 'package:worker/features/projects/domain/entities/project_submission.dart';
|
import 'package:worker/features/projects/domain/entities/project_submission.dart';
|
||||||
import 'package:worker/features/projects/domain/repositories/submissions_repository.dart';
|
import 'package:worker/features/projects/domain/repositories/submissions_repository.dart';
|
||||||
@@ -16,10 +19,12 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
|
|||||||
const SubmissionsRepositoryImpl(
|
const SubmissionsRepositoryImpl(
|
||||||
this._remoteDataSource,
|
this._remoteDataSource,
|
||||||
this._statusLocalDataSource,
|
this._statusLocalDataSource,
|
||||||
|
this._progressLocalDataSource,
|
||||||
);
|
);
|
||||||
|
|
||||||
final SubmissionsRemoteDataSource _remoteDataSource;
|
final SubmissionsRemoteDataSource _remoteDataSource;
|
||||||
final ProjectStatusLocalDataSource _statusLocalDataSource;
|
final ProjectStatusLocalDataSource _statusLocalDataSource;
|
||||||
|
final ProjectProgressLocalDataSource _progressLocalDataSource;
|
||||||
|
|
||||||
/// Get project status list with cache-first pattern
|
/// 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
|
@override
|
||||||
Future<List<ProjectSubmission>> getSubmissions({
|
Future<List<ProjectSubmission>> getSubmissions({
|
||||||
int limitStart = 0,
|
int limitStart = 0,
|
||||||
@@ -84,4 +137,28 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
|
|||||||
rethrow;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
lib/features/projects/domain/entities/project_progress.dart
Normal file
35
lib/features/projects/domain/entities/project_progress.dart
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/// Project Progress Entity
|
||||||
|
///
|
||||||
|
/// Represents a construction progress stage from the API.
|
||||||
|
/// Used for dropdown selection when creating/updating project submissions.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Project Progress Entity
|
||||||
|
///
|
||||||
|
/// Contains construction progress stages:
|
||||||
|
/// - Chưa khởi công (Not started)
|
||||||
|
/// - Khởi công móng (Foundation started)
|
||||||
|
/// - Đang phần thô (Rough construction)
|
||||||
|
/// - Đang hoàn thiện (Finishing)
|
||||||
|
/// - Cất nóc (Roofing complete)
|
||||||
|
/// - Hoàn thiện (Completed)
|
||||||
|
class ProjectProgress extends Equatable {
|
||||||
|
/// Unique identifier (API: name)
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// Progress status label in Vietnamese (API: status)
|
||||||
|
final String status;
|
||||||
|
|
||||||
|
const ProjectProgress({
|
||||||
|
required this.id,
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, status];
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ProjectProgress(id: $id, status: $status)';
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
/// Repository interface for project submissions operations.
|
/// Repository interface for project submissions operations.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
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_status.dart';
|
||||||
import 'package:worker/features/projects/domain/entities/project_submission.dart';
|
import 'package:worker/features/projects/domain/entities/project_submission.dart';
|
||||||
|
|
||||||
@@ -20,9 +22,32 @@ abstract class SubmissionsRepository {
|
|||||||
bool forceRefresh = false,
|
bool forceRefresh = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Get list of construction progress stages
|
||||||
|
///
|
||||||
|
/// Uses cache-first pattern:
|
||||||
|
/// - Returns cached data if available
|
||||||
|
/// - Fetches from API and updates cache
|
||||||
|
/// - [forceRefresh] bypasses cache and fetches fresh data
|
||||||
|
Future<List<ProjectProgress>> getProjectProgressList({
|
||||||
|
bool forceRefresh = false,
|
||||||
|
});
|
||||||
|
|
||||||
/// Get all project submissions for the current user
|
/// Get all project submissions for the current user
|
||||||
Future<List<ProjectSubmission>> getSubmissions({
|
Future<List<ProjectSubmission>> getSubmissions({
|
||||||
int limitStart = 0,
|
int limitStart = 0,
|
||||||
int limitPageLength = 0,
|
int limitPageLength = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Save (create/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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
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/domain/entities/project_progress.dart';
|
||||||
|
import 'package:worker/features/projects/presentation/providers/submissions_provider.dart';
|
||||||
|
|
||||||
/// Project Submission Create Page
|
/// Project Submission Create Page
|
||||||
class SubmissionCreatePage extends ConsumerStatefulWidget {
|
class SubmissionCreatePage extends ConsumerStatefulWidget {
|
||||||
@@ -35,10 +38,10 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
final _descriptionController = TextEditingController();
|
final _descriptionController = TextEditingController();
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
String? _selectedProgress;
|
ProjectProgress? _selectedProgress;
|
||||||
DateTime? _expectedStartDate;
|
DateTime? _expectedStartDate;
|
||||||
final List<File> _uploadedFiles = [];
|
final List<File> _uploadedFiles = [];
|
||||||
bool _showStartDateField = false;
|
bool _isSubmitting = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -217,10 +220,8 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
|
|
||||||
_buildProgressDropdown(),
|
_buildProgressDropdown(),
|
||||||
|
|
||||||
if (_showStartDateField) ...[
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildDateField(),
|
_buildExpectedDateField(),
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -434,6 +435,8 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProgressDropdown() {
|
Widget _buildProgressDropdown() {
|
||||||
|
final progressListAsync = ref.watch(projectProgressListProvider);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -457,8 +460,9 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
DropdownButtonFormField<String>(
|
progressListAsync.when(
|
||||||
value: _selectedProgress,
|
data: (progressList) => DropdownButtonFormField<ProjectProgress>(
|
||||||
|
initialValue: _selectedProgress,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.white,
|
fillColor: AppColors.white,
|
||||||
@@ -476,49 +480,73 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
hint: const Text('Chọn tiến độ'),
|
hint: const Text('Chọn tiến độ'),
|
||||||
items: const [
|
items: progressList
|
||||||
DropdownMenuItem(
|
.map((progress) => DropdownMenuItem<ProjectProgress>(
|
||||||
value: 'not-started',
|
value: progress,
|
||||||
child: Text('Chưa khởi công'),
|
child: Text(progress.status),
|
||||||
),
|
))
|
||||||
DropdownMenuItem(
|
.toList(),
|
||||||
value: 'foundation',
|
|
||||||
child: Text('Khởi công móng'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'rough-construction',
|
|
||||||
child: Text('Đang phần thô'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'finishing',
|
|
||||||
child: Text('Đang hoàn thiện'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'topped-out',
|
|
||||||
child: Text('Cất nóc'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedProgress = value;
|
_selectedProgress = value;
|
||||||
_showStartDateField = value == 'not-started';
|
|
||||||
if (!_showStartDateField) {
|
|
||||||
_expectedStartDate = null;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null) {
|
||||||
return 'Vui lòng chọn tiến độ công trình';
|
return 'Vui lòng chọn tiến độ công trình';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
loading: () => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.white,
|
||||||
|
border: Border.all(color: AppColors.grey100),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Text('Đang tải...', style: TextStyle(color: AppColors.grey500)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (error, _) => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.white,
|
||||||
|
border: Border.all(color: AppColors.danger),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const FaIcon(FontAwesomeIcons.circleExclamation,
|
||||||
|
size: 16, color: AppColors.danger),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Expanded(
|
||||||
|
child: Text('Không thể tải danh sách tiến độ',
|
||||||
|
style: TextStyle(color: AppColors.danger)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () =>
|
||||||
|
ref.invalidate(projectProgressListProvider),
|
||||||
|
child: const Text('Thử lại'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDateField() {
|
Widget _buildExpectedDateField() {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -532,7 +560,7 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: _pickDate,
|
onTap: _pickExpectedDate,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -545,7 +573,7 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_expectedStartDate != null
|
_expectedStartDate != null
|
||||||
? '${_expectedStartDate!.day}/${_expectedStartDate!.month}/${_expectedStartDate!.year}'
|
? '${_expectedStartDate!.day.toString().padLeft(2, '0')}/${_expectedStartDate!.month.toString().padLeft(2, '0')}/${_expectedStartDate!.year}'
|
||||||
: 'Chọn ngày',
|
: 'Chọn ngày',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _expectedStartDate != null
|
color: _expectedStartDate != null
|
||||||
@@ -571,14 +599,29 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
final fileSizeInBytes = file.lengthSync();
|
final fileSizeInBytes = file.lengthSync();
|
||||||
final fileSizeInMB = (fileSizeInBytes / (1024 * 1024)).toStringAsFixed(2);
|
final fileSizeInMB = (fileSizeInBytes / (1024 * 1024)).toStringAsFixed(2);
|
||||||
|
|
||||||
|
// Get upload state for this file
|
||||||
|
final uploadStates = ref.watch(uploadProjectFilesProvider);
|
||||||
|
final isUploading = index < uploadStates.length && uploadStates[index].isUploading;
|
||||||
|
final isUploaded = index < uploadStates.length && uploadStates[index].isUploaded;
|
||||||
|
final hasError = index < uploadStates.length && uploadStates[index].error != null;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F9FA),
|
color: const Color(0xFFF8F9FA),
|
||||||
border: Border.all(color: AppColors.grey100),
|
border: Border.all(
|
||||||
|
color: hasError
|
||||||
|
? AppColors.danger
|
||||||
|
: isUploaded
|
||||||
|
? AppColors.success
|
||||||
|
: AppColors.grey100,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Image with upload overlay
|
||||||
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
@@ -601,6 +644,45 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Uploading overlay
|
||||||
|
if (isUploading)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Uploaded checkmark overlay
|
||||||
|
if (isUploaded)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.success.withValues(alpha: 0.7),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.check,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -617,15 +699,27 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'${fileSizeInMB}MB',
|
isUploading
|
||||||
style: const TextStyle(
|
? 'Đang tải lên...'
|
||||||
|
: isUploaded
|
||||||
|
? 'Đã tải lên'
|
||||||
|
: hasError
|
||||||
|
? 'Lỗi tải lên'
|
||||||
|
: '${fileSizeInMB}MB',
|
||||||
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: hasError
|
||||||
|
? AppColors.danger
|
||||||
|
: isUploaded
|
||||||
|
? AppColors.success
|
||||||
|
: AppColors.grey500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Only show remove button when not uploading
|
||||||
|
if (!_isSubmitting)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const FaIcon(
|
icon: const FaIcon(
|
||||||
FontAwesomeIcons.xmark,
|
FontAwesomeIcons.xmark,
|
||||||
@@ -648,16 +742,27 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 48,
|
height: 48,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _handleSubmit,
|
onPressed: _isSubmitting ? null : _handleSubmit,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: AppColors.primaryBlue,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: AppColors.white,
|
||||||
|
disabledBackgroundColor: AppColors.primaryBlue.withValues(alpha: 0.6),
|
||||||
|
disabledForegroundColor: AppColors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: _isSubmitting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(AppColors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FaIcon(FontAwesomeIcons.paperPlane, size: 16),
|
FaIcon(FontAwesomeIcons.paperPlane, size: 16),
|
||||||
@@ -675,12 +780,12 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickDate() async {
|
Future<void> _pickExpectedDate() async {
|
||||||
final date = await showDatePicker(
|
final date = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: DateTime.now(),
|
initialDate: _expectedStartDate ?? DateTime.now(),
|
||||||
firstDate: DateTime.now(),
|
firstDate: DateTime.now(),
|
||||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
lastDate: DateTime.now().add(const Duration(days: 365 * 3)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (date != null) {
|
if (date != null) {
|
||||||
@@ -725,7 +830,19 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleSubmit() async {
|
Future<void> _handleSubmit() async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
// Validate progress selection
|
||||||
|
if (_selectedProgress == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Vui lòng chọn tiến độ công trình'),
|
||||||
|
backgroundColor: AppColors.danger,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -744,15 +861,75 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed == true && mounted) {
|
if (confirmed != true || !mounted) return;
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse area as double
|
||||||
|
final area = double.tryParse(_areaController.text.trim()) ?? 0.0;
|
||||||
|
|
||||||
|
// Create submission request
|
||||||
|
final request = ProjectSubmissionRequest(
|
||||||
|
designedArea: _projectNameController.text.trim(),
|
||||||
|
addressOfProject: _addressController.text.trim(),
|
||||||
|
projectOwner: _ownerController.text.trim(),
|
||||||
|
designFirm: _designUnitController.text.trim().isNotEmpty
|
||||||
|
? _designUnitController.text.trim()
|
||||||
|
: null,
|
||||||
|
contractionContractor: _constructionUnitController.text.trim().isNotEmpty
|
||||||
|
? _constructionUnitController.text.trim()
|
||||||
|
: null,
|
||||||
|
designArea: area,
|
||||||
|
productsIncludedInTheDesign: _productsController.text.trim(),
|
||||||
|
projectProgress: _selectedProgress!.id, // Use ProjectProgress.id (name from API)
|
||||||
|
expectedCommencementDate: _expectedStartDate,
|
||||||
|
description: _descriptionController.text.trim().isNotEmpty
|
||||||
|
? _descriptionController.text.trim()
|
||||||
|
: null,
|
||||||
|
requestDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 1: Save project and get project name
|
||||||
|
final projectName = await ref.read(saveSubmissionProvider.notifier).save(request);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// Step 2: Upload files if any
|
||||||
|
if (_uploadedFiles.isNotEmpty) {
|
||||||
|
// Initialize upload provider with file paths
|
||||||
|
final filePaths = _uploadedFiles.map((f) => f.path).toList();
|
||||||
|
ref.read(uploadProjectFilesProvider.notifier).initFiles(filePaths);
|
||||||
|
|
||||||
|
// Upload all files
|
||||||
|
await ref.read(uploadProjectFilesProvider.notifier).uploadAll(projectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const 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.',
|
'Đă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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context, true); // Return true to indicate success
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Lỗi: ${e.toString().replaceAll('Exception: ', '')}'),
|
||||||
|
backgroundColor: AppColors.danger,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
// Clear upload state
|
||||||
|
ref.read(uploadProjectFilesProvider.notifier).clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,13 @@ class SubmissionsPage extends ConsumerWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.plus, color: Colors.black, size: 20),
|
icon: const FaIcon(FontAwesomeIcons.plus, color: Colors.black, size: 20),
|
||||||
onPressed: () => context.push(RouteNames.submissionCreate),
|
onPressed: () async {
|
||||||
|
final result = await context.push<bool>(RouteNames.submissionCreate);
|
||||||
|
if (result == true) {
|
||||||
|
// Refresh submissions list after successful creation
|
||||||
|
ref.invalidate(allSubmissionsProvider);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: AppSpacing.sm),
|
const SizedBox(width: AppSpacing.sm),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ library;
|
|||||||
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:worker/core/network/dio_client.dart';
|
import 'package:worker/core/network/dio_client.dart';
|
||||||
|
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/project_status_local_datasource.dart';
|
||||||
import 'package:worker/features/projects/data/datasources/submissions_remote_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/data/repositories/submissions_repository_impl.dart';
|
import 'package:worker/features/projects/data/repositories/submissions_repository_impl.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_status.dart';
|
||||||
import 'package:worker/features/projects/domain/entities/project_submission.dart';
|
import 'package:worker/features/projects/domain/entities/project_submission.dart';
|
||||||
import 'package:worker/features/projects/domain/repositories/submissions_repository.dart';
|
import 'package:worker/features/projects/domain/repositories/submissions_repository.dart';
|
||||||
@@ -20,6 +23,12 @@ ProjectStatusLocalDataSource projectStatusLocalDataSource(Ref ref) {
|
|||||||
return ProjectStatusLocalDataSource();
|
return ProjectStatusLocalDataSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Project Progress Local Data Source Provider
|
||||||
|
@riverpod
|
||||||
|
ProjectProgressLocalDataSource projectProgressLocalDataSource(Ref ref) {
|
||||||
|
return ProjectProgressLocalDataSource();
|
||||||
|
}
|
||||||
|
|
||||||
/// Submissions Remote Data Source Provider
|
/// Submissions Remote Data Source Provider
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<SubmissionsRemoteDataSource> submissionsRemoteDataSource(Ref ref) async {
|
Future<SubmissionsRemoteDataSource> submissionsRemoteDataSource(Ref ref) async {
|
||||||
@@ -32,7 +41,12 @@ Future<SubmissionsRemoteDataSource> submissionsRemoteDataSource(Ref ref) async {
|
|||||||
Future<SubmissionsRepository> submissionsRepository(Ref ref) async {
|
Future<SubmissionsRepository> submissionsRepository(Ref ref) async {
|
||||||
final remoteDataSource = await ref.watch(submissionsRemoteDataSourceProvider.future);
|
final remoteDataSource = await ref.watch(submissionsRemoteDataSourceProvider.future);
|
||||||
final statusLocalDataSource = ref.watch(projectStatusLocalDataSourceProvider);
|
final statusLocalDataSource = ref.watch(projectStatusLocalDataSourceProvider);
|
||||||
return SubmissionsRepositoryImpl(remoteDataSource, statusLocalDataSource);
|
final progressLocalDataSource = ref.watch(projectProgressLocalDataSourceProvider);
|
||||||
|
return SubmissionsRepositoryImpl(
|
||||||
|
remoteDataSource,
|
||||||
|
statusLocalDataSource,
|
||||||
|
progressLocalDataSource,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Project Status List Provider
|
/// Project Status List Provider
|
||||||
@@ -57,16 +71,39 @@ class ProjectStatusList extends _$ProjectStatusList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Project Progress List Provider
|
||||||
|
///
|
||||||
|
/// Fetches construction progress stages from API with cache-first pattern.
|
||||||
|
/// Used for dropdown selection when creating/updating project submissions.
|
||||||
|
@riverpod
|
||||||
|
class ProjectProgressList extends _$ProjectProgressList {
|
||||||
|
@override
|
||||||
|
Future<List<ProjectProgress>> build() async {
|
||||||
|
final repository = await ref.watch(submissionsRepositoryProvider.future);
|
||||||
|
return repository.getProjectProgressList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh progress list from remote (force refresh)
|
||||||
|
Future<void> refresh() async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
state = await AsyncValue.guard(() async {
|
||||||
|
final repository = await ref.read(submissionsRepositoryProvider.future);
|
||||||
|
return repository.getProjectProgressList(forceRefresh: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// All Submissions Provider
|
/// All Submissions Provider
|
||||||
///
|
///
|
||||||
/// Fetches and manages submissions data from remote.
|
/// Fetches and manages submissions data from remote.
|
||||||
/// Waits for project status list to be loaded first.
|
/// Waits for project status list and progress list to be loaded first.
|
||||||
@riverpod
|
@riverpod
|
||||||
class AllSubmissions extends _$AllSubmissions {
|
class AllSubmissions extends _$AllSubmissions {
|
||||||
@override
|
@override
|
||||||
Future<List<ProjectSubmission>> build() async {
|
Future<List<ProjectSubmission>> build() async {
|
||||||
// Ensure status list is loaded first (for filter options)
|
// Ensure status list and progress list are loaded first (for filter options)
|
||||||
await ref.watch(projectStatusListProvider.future);
|
await ref.watch(projectStatusListProvider.future);
|
||||||
|
await ref.watch(projectProgressListProvider.future);
|
||||||
|
|
||||||
// Then fetch submissions
|
// Then fetch submissions
|
||||||
final repository = await ref.watch(submissionsRepositoryProvider.future);
|
final repository = await ref.watch(submissionsRepositoryProvider.future);
|
||||||
@@ -77,8 +114,9 @@ class AllSubmissions extends _$AllSubmissions {
|
|||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
state = await AsyncValue.guard(() async {
|
state = await AsyncValue.guard(() async {
|
||||||
// Also refresh status list
|
// Also refresh status list and progress list
|
||||||
await ref.read(projectStatusListProvider.notifier).refresh();
|
await ref.read(projectStatusListProvider.notifier).refresh();
|
||||||
|
await ref.read(projectProgressListProvider.notifier).refresh();
|
||||||
|
|
||||||
final repository = await ref.read(submissionsRepositoryProvider.future);
|
final repository = await ref.read(submissionsRepositoryProvider.future);
|
||||||
return repository.getSubmissions();
|
return repository.getSubmissions();
|
||||||
@@ -154,3 +192,152 @@ AsyncValue<List<ProjectSubmission>> filteredSubmissions(Ref ref) {
|
|||||||
return filtered;
|
return filtered;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Save Submission Provider
|
||||||
|
///
|
||||||
|
/// Handles creating new project submissions via API.
|
||||||
|
@riverpod
|
||||||
|
class SaveSubmission extends _$SaveSubmission {
|
||||||
|
@override
|
||||||
|
AsyncValue<void> build() {
|
||||||
|
return const AsyncValue.data(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a new project submission
|
||||||
|
///
|
||||||
|
/// Returns the project name (ID) if successful, throws exception on failure.
|
||||||
|
Future<String> save(ProjectSubmissionRequest request) async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
try {
|
||||||
|
final repository = await ref.read(submissionsRepositoryProvider.future);
|
||||||
|
if (!ref.mounted) throw Exception('Provider disposed');
|
||||||
|
|
||||||
|
final projectName = await repository.saveSubmission(request);
|
||||||
|
if (!ref.mounted) return projectName;
|
||||||
|
|
||||||
|
state = const AsyncValue.data(null);
|
||||||
|
|
||||||
|
// Refresh submissions list after successful save
|
||||||
|
ref.invalidate(allSubmissionsProvider);
|
||||||
|
|
||||||
|
return projectName;
|
||||||
|
} catch (e, st) {
|
||||||
|
if (ref.mounted) {
|
||||||
|
state = AsyncValue.error(e, st);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload state for tracking individual file uploads
|
||||||
|
class FileUploadState {
|
||||||
|
final String filePath;
|
||||||
|
final bool isUploading;
|
||||||
|
final bool isUploaded;
|
||||||
|
final String? fileUrl;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
const FileUploadState({
|
||||||
|
required this.filePath,
|
||||||
|
this.isUploading = false,
|
||||||
|
this.isUploaded = false,
|
||||||
|
this.fileUrl,
|
||||||
|
this.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
FileUploadState copyWith({
|
||||||
|
bool? isUploading,
|
||||||
|
bool? isUploaded,
|
||||||
|
String? fileUrl,
|
||||||
|
String? error,
|
||||||
|
}) {
|
||||||
|
return FileUploadState(
|
||||||
|
filePath: filePath,
|
||||||
|
isUploading: isUploading ?? this.isUploading,
|
||||||
|
isUploaded: isUploaded ?? this.isUploaded,
|
||||||
|
fileUrl: fileUrl ?? this.fileUrl,
|
||||||
|
error: error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload Project Files Provider
|
||||||
|
///
|
||||||
|
/// Handles uploading multiple files for a project submission.
|
||||||
|
/// Tracks upload state for each file individually.
|
||||||
|
@riverpod
|
||||||
|
class UploadProjectFiles extends _$UploadProjectFiles {
|
||||||
|
@override
|
||||||
|
List<FileUploadState> build() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize with file paths
|
||||||
|
void initFiles(List<String> filePaths) {
|
||||||
|
state = filePaths
|
||||||
|
.map((path) => FileUploadState(filePath: path))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload all files for a project
|
||||||
|
/// Returns list of uploaded file URLs
|
||||||
|
Future<List<String>> uploadAll(String projectName) async {
|
||||||
|
final uploadedUrls = <String>[];
|
||||||
|
|
||||||
|
for (var i = 0; i < state.length; i++) {
|
||||||
|
if (!ref.mounted) break;
|
||||||
|
|
||||||
|
// Mark as uploading
|
||||||
|
state = [
|
||||||
|
...state.sublist(0, i),
|
||||||
|
state[i].copyWith(isUploading: true),
|
||||||
|
...state.sublist(i + 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final repository = await ref.read(submissionsRepositoryProvider.future);
|
||||||
|
if (!ref.mounted) break;
|
||||||
|
|
||||||
|
final fileUrl = await repository.uploadProjectFile(
|
||||||
|
projectName: projectName,
|
||||||
|
filePath: state[i].filePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ref.mounted) break;
|
||||||
|
|
||||||
|
// Mark as uploaded
|
||||||
|
state = [
|
||||||
|
...state.sublist(0, i),
|
||||||
|
state[i].copyWith(
|
||||||
|
isUploading: false,
|
||||||
|
isUploaded: true,
|
||||||
|
fileUrl: fileUrl,
|
||||||
|
),
|
||||||
|
...state.sublist(i + 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
uploadedUrls.add(fileUrl);
|
||||||
|
} catch (e) {
|
||||||
|
if (!ref.mounted) break;
|
||||||
|
|
||||||
|
// Mark as failed
|
||||||
|
state = [
|
||||||
|
...state.sublist(0, i),
|
||||||
|
state[i].copyWith(
|
||||||
|
isUploading: false,
|
||||||
|
error: e.toString(),
|
||||||
|
),
|
||||||
|
...state.sublist(i + 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadedUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all files
|
||||||
|
void clear() {
|
||||||
|
state = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,6 +62,62 @@ final class ProjectStatusLocalDataSourceProvider
|
|||||||
String _$projectStatusLocalDataSourceHash() =>
|
String _$projectStatusLocalDataSourceHash() =>
|
||||||
r'c57291e51bd390f9524369860c241d7a0a90fdbf';
|
r'c57291e51bd390f9524369860c241d7a0a90fdbf';
|
||||||
|
|
||||||
|
/// Project Progress Local Data Source Provider
|
||||||
|
|
||||||
|
@ProviderFor(projectProgressLocalDataSource)
|
||||||
|
const projectProgressLocalDataSourceProvider =
|
||||||
|
ProjectProgressLocalDataSourceProvider._();
|
||||||
|
|
||||||
|
/// Project Progress Local Data Source Provider
|
||||||
|
|
||||||
|
final class ProjectProgressLocalDataSourceProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
ProjectProgressLocalDataSource,
|
||||||
|
ProjectProgressLocalDataSource,
|
||||||
|
ProjectProgressLocalDataSource
|
||||||
|
>
|
||||||
|
with $Provider<ProjectProgressLocalDataSource> {
|
||||||
|
/// Project Progress Local Data Source Provider
|
||||||
|
const ProjectProgressLocalDataSourceProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'projectProgressLocalDataSourceProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$projectProgressLocalDataSourceHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<ProjectProgressLocalDataSource> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ProjectProgressLocalDataSource create(Ref ref) {
|
||||||
|
return projectProgressLocalDataSource(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(ProjectProgressLocalDataSource value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<ProjectProgressLocalDataSource>(
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$projectProgressLocalDataSourceHash() =>
|
||||||
|
r'653d03b47f5642f3391e7a312649a2603489b224';
|
||||||
|
|
||||||
/// Submissions Remote Data Source Provider
|
/// Submissions Remote Data Source Provider
|
||||||
|
|
||||||
@ProviderFor(submissionsRemoteDataSource)
|
@ProviderFor(submissionsRemoteDataSource)
|
||||||
@@ -155,7 +211,7 @@ final class SubmissionsRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$submissionsRepositoryHash() =>
|
String _$submissionsRepositoryHash() =>
|
||||||
r'd8261cc538c1fdaa47064e4945302b80f49098bb';
|
r'652208a4ef93cde9b40ae66164d44bba786dfed0';
|
||||||
|
|
||||||
/// Project Status List Provider
|
/// Project Status List Provider
|
||||||
///
|
///
|
||||||
@@ -221,10 +277,80 @@ abstract class _$ProjectStatusList extends $AsyncNotifier<List<ProjectStatus>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Project Progress List Provider
|
||||||
|
///
|
||||||
|
/// Fetches construction progress stages from API with cache-first pattern.
|
||||||
|
/// Used for dropdown selection when creating/updating project submissions.
|
||||||
|
|
||||||
|
@ProviderFor(ProjectProgressList)
|
||||||
|
const projectProgressListProvider = ProjectProgressListProvider._();
|
||||||
|
|
||||||
|
/// Project Progress List Provider
|
||||||
|
///
|
||||||
|
/// Fetches construction progress stages from API with cache-first pattern.
|
||||||
|
/// Used for dropdown selection when creating/updating project submissions.
|
||||||
|
final class ProjectProgressListProvider
|
||||||
|
extends $AsyncNotifierProvider<ProjectProgressList, List<ProjectProgress>> {
|
||||||
|
/// Project Progress List Provider
|
||||||
|
///
|
||||||
|
/// Fetches construction progress stages from API with cache-first pattern.
|
||||||
|
/// Used for dropdown selection when creating/updating project submissions.
|
||||||
|
const ProjectProgressListProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'projectProgressListProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$projectProgressListHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
ProjectProgressList create() => ProjectProgressList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$projectProgressListHash() =>
|
||||||
|
r'5ee1c23f90bfa61237f38a6b72c353f0ecb7a2a9';
|
||||||
|
|
||||||
|
/// Project Progress List Provider
|
||||||
|
///
|
||||||
|
/// Fetches construction progress stages from API with cache-first pattern.
|
||||||
|
/// Used for dropdown selection when creating/updating project submissions.
|
||||||
|
|
||||||
|
abstract class _$ProjectProgressList
|
||||||
|
extends $AsyncNotifier<List<ProjectProgress>> {
|
||||||
|
FutureOr<List<ProjectProgress>> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref =
|
||||||
|
this.ref
|
||||||
|
as $Ref<AsyncValue<List<ProjectProgress>>, List<ProjectProgress>>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<
|
||||||
|
AsyncValue<List<ProjectProgress>>,
|
||||||
|
List<ProjectProgress>
|
||||||
|
>,
|
||||||
|
AsyncValue<List<ProjectProgress>>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// All Submissions Provider
|
/// All Submissions Provider
|
||||||
///
|
///
|
||||||
/// Fetches and manages submissions data from remote.
|
/// Fetches and manages submissions data from remote.
|
||||||
/// Waits for project status list to be loaded first.
|
/// Waits for project status list and progress list to be loaded first.
|
||||||
|
|
||||||
@ProviderFor(AllSubmissions)
|
@ProviderFor(AllSubmissions)
|
||||||
const allSubmissionsProvider = AllSubmissionsProvider._();
|
const allSubmissionsProvider = AllSubmissionsProvider._();
|
||||||
@@ -232,13 +358,13 @@ const allSubmissionsProvider = AllSubmissionsProvider._();
|
|||||||
/// All Submissions Provider
|
/// All Submissions Provider
|
||||||
///
|
///
|
||||||
/// Fetches and manages submissions data from remote.
|
/// Fetches and manages submissions data from remote.
|
||||||
/// Waits for project status list to be loaded first.
|
/// Waits for project status list and progress list to be loaded first.
|
||||||
final class AllSubmissionsProvider
|
final class AllSubmissionsProvider
|
||||||
extends $AsyncNotifierProvider<AllSubmissions, List<ProjectSubmission>> {
|
extends $AsyncNotifierProvider<AllSubmissions, List<ProjectSubmission>> {
|
||||||
/// All Submissions Provider
|
/// All Submissions Provider
|
||||||
///
|
///
|
||||||
/// Fetches and manages submissions data from remote.
|
/// Fetches and manages submissions data from remote.
|
||||||
/// Waits for project status list to be loaded first.
|
/// Waits for project status list and progress list to be loaded first.
|
||||||
const AllSubmissionsProvider._()
|
const AllSubmissionsProvider._()
|
||||||
: super(
|
: super(
|
||||||
from: null,
|
from: null,
|
||||||
@@ -258,12 +384,12 @@ final class AllSubmissionsProvider
|
|||||||
AllSubmissions create() => AllSubmissions();
|
AllSubmissions create() => AllSubmissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$allSubmissionsHash() => r'a4a7fb0d2953efb21e2e6343429f7550c763ea85';
|
String _$allSubmissionsHash() => r'ab0f1ffdc5e6bdb62dbd56ff3e586ecc1ff05bea';
|
||||||
|
|
||||||
/// All Submissions Provider
|
/// All Submissions Provider
|
||||||
///
|
///
|
||||||
/// Fetches and manages submissions data from remote.
|
/// Fetches and manages submissions data from remote.
|
||||||
/// Waits for project status list to be loaded first.
|
/// Waits for project status list and progress list to be loaded first.
|
||||||
|
|
||||||
abstract class _$AllSubmissions
|
abstract class _$AllSubmissions
|
||||||
extends $AsyncNotifier<List<ProjectSubmission>> {
|
extends $AsyncNotifier<List<ProjectSubmission>> {
|
||||||
@@ -442,3 +568,142 @@ final class FilteredSubmissionsProvider
|
|||||||
|
|
||||||
String _$filteredSubmissionsHash() =>
|
String _$filteredSubmissionsHash() =>
|
||||||
r'5be22b3242426c6b0c2f9778eaee5c7cf23e4814';
|
r'5be22b3242426c6b0c2f9778eaee5c7cf23e4814';
|
||||||
|
|
||||||
|
/// Save Submission Provider
|
||||||
|
///
|
||||||
|
/// Handles creating new project submissions via API.
|
||||||
|
|
||||||
|
@ProviderFor(SaveSubmission)
|
||||||
|
const saveSubmissionProvider = SaveSubmissionProvider._();
|
||||||
|
|
||||||
|
/// Save Submission Provider
|
||||||
|
///
|
||||||
|
/// Handles creating new project submissions via API.
|
||||||
|
final class SaveSubmissionProvider
|
||||||
|
extends $NotifierProvider<SaveSubmission, AsyncValue<void>> {
|
||||||
|
/// Save Submission Provider
|
||||||
|
///
|
||||||
|
/// Handles creating new project submissions via API.
|
||||||
|
const SaveSubmissionProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'saveSubmissionProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$saveSubmissionHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
SaveSubmission create() => SaveSubmission();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(AsyncValue<void> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<AsyncValue<void>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$saveSubmissionHash() => r'64afa1a9662c36431c143c46a8ca34a786cb0860';
|
||||||
|
|
||||||
|
/// Save Submission Provider
|
||||||
|
///
|
||||||
|
/// Handles creating new project submissions via API.
|
||||||
|
|
||||||
|
abstract class _$SaveSubmission extends $Notifier<AsyncValue<void>> {
|
||||||
|
AsyncValue<void> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<AsyncValue<void>, AsyncValue<void>>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<AsyncValue<void>, AsyncValue<void>>,
|
||||||
|
AsyncValue<void>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload Project Files Provider
|
||||||
|
///
|
||||||
|
/// Handles uploading multiple files for a project submission.
|
||||||
|
/// Tracks upload state for each file individually.
|
||||||
|
|
||||||
|
@ProviderFor(UploadProjectFiles)
|
||||||
|
const uploadProjectFilesProvider = UploadProjectFilesProvider._();
|
||||||
|
|
||||||
|
/// Upload Project Files Provider
|
||||||
|
///
|
||||||
|
/// Handles uploading multiple files for a project submission.
|
||||||
|
/// Tracks upload state for each file individually.
|
||||||
|
final class UploadProjectFilesProvider
|
||||||
|
extends $NotifierProvider<UploadProjectFiles, List<FileUploadState>> {
|
||||||
|
/// Upload Project Files Provider
|
||||||
|
///
|
||||||
|
/// Handles uploading multiple files for a project submission.
|
||||||
|
/// Tracks upload state for each file individually.
|
||||||
|
const UploadProjectFilesProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'uploadProjectFilesProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$uploadProjectFilesHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
UploadProjectFiles create() => UploadProjectFiles();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(List<FileUploadState> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<List<FileUploadState>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$uploadProjectFilesHash() =>
|
||||||
|
r'd6219bc1f0b0d6ac70b9e3cea731267c82a68e1f';
|
||||||
|
|
||||||
|
/// Upload Project Files Provider
|
||||||
|
///
|
||||||
|
/// Handles uploading multiple files for a project submission.
|
||||||
|
/// Tracks upload state for each file individually.
|
||||||
|
|
||||||
|
abstract class _$UploadProjectFiles extends $Notifier<List<FileUploadState>> {
|
||||||
|
List<FileUploadState> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<List<FileUploadState>, List<FileUploadState>>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<List<FileUploadState>, List<FileUploadState>>,
|
||||||
|
List<FileUploadState>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import 'package:worker/features/products/data/models/category_model.dart';
|
|||||||
import 'package:worker/features/products/data/models/product_model.dart';
|
import 'package:worker/features/products/data/models/product_model.dart';
|
||||||
import 'package:worker/features/products/data/models/stock_level_model.dart';
|
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_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/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';
|
||||||
@@ -77,6 +78,7 @@ extension HiveRegistrar on HiveInterface {
|
|||||||
registerAdapter(PointsRecordModelAdapter());
|
registerAdapter(PointsRecordModelAdapter());
|
||||||
registerAdapter(PointsStatusAdapter());
|
registerAdapter(PointsStatusAdapter());
|
||||||
registerAdapter(ProductModelAdapter());
|
registerAdapter(ProductModelAdapter());
|
||||||
|
registerAdapter(ProjectProgressModelAdapter());
|
||||||
registerAdapter(ProjectStatusModelAdapter());
|
registerAdapter(ProjectStatusModelAdapter());
|
||||||
registerAdapter(ProjectSubmissionModelAdapter());
|
registerAdapter(ProjectSubmissionModelAdapter());
|
||||||
registerAdapter(ProjectTypeAdapter());
|
registerAdapter(ProjectTypeAdapter());
|
||||||
@@ -137,6 +139,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
|||||||
registerAdapter(PointsRecordModelAdapter());
|
registerAdapter(PointsRecordModelAdapter());
|
||||||
registerAdapter(PointsStatusAdapter());
|
registerAdapter(PointsStatusAdapter());
|
||||||
registerAdapter(ProductModelAdapter());
|
registerAdapter(ProductModelAdapter());
|
||||||
|
registerAdapter(ProjectProgressModelAdapter());
|
||||||
registerAdapter(ProjectStatusModelAdapter());
|
registerAdapter(ProjectStatusModelAdapter());
|
||||||
registerAdapter(ProjectSubmissionModelAdapter());
|
registerAdapter(ProjectSubmissionModelAdapter());
|
||||||
registerAdapter(ProjectTypeAdapter());
|
registerAdapter(ProjectTypeAdapter());
|
||||||
|
|||||||
Reference in New Issue
Block a user