Compare commits

..

3 Commits

Author SHA1 Message Date
Phuoc Nguyen
6e7e848ad6 submission 2025-11-27 17:58:13 +07:00
Phuoc Nguyen
b6cb9e865a create submission 2025-11-27 16:56:01 +07:00
Phuoc Nguyen
ba04576750 add 2025-11-27 14:59:48 +07:00
32 changed files with 3031 additions and 878 deletions

182
docs/projects.sh Normal file
View File

@@ -0,0 +1,182 @@
#get status list
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_project_status_list' \
--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 '{
"limit_start": 0,
"limit_page_length": 0
}'
#response
{
"message": [
{
"status": "Pending approval",
"label": "Chờ phê duyệt",
"color": "Warning",
"index": 1
},
{
"status": "Approved",
"label": "Đã được phê duyệt",
"color": "Success",
"index": 2
},
{
"status": "Rejected",
"label": "Từ chối",
"color": "Danger",
"index": 3
},
{
"status": "Cancelled",
"label": "HỦY BỎ",
"color": "Danger",
"index": 4
}
]
}
#get project list
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_list' \
--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 '{
"limit_start": 0,
"limit_page_length": 0
}'
#response
{
"message": [
{
"name": "p9ti8veq2g",
"designed_area": "Sunrise Villa Phase 355",
"design_area": 350.5,
"request_date": "2025-11-26 09:30:00",
"status": "Đã được phê duyệt",
"reason_for_rejection": null,
"status_color": "Success"
}
]
}
#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"'
#get detail of a project
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_detail' \
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
--header 'Content-Type: application/json' \
--data '{
"name": "#DA00011"
}'
#response
{
"message": {
"success": true,
"data": {
"name": "#DA00011",
"designed_area": "f67gg7",
"address_of_project": "7fucuv",
"project_owner": "cycu",
"design_firm": null,
"contruction_contractor": null,
"design_area": 2585.0,
"products_included_in_the_design": "thy",
"project_progress": "k1mr565o91",
"expected_commencement_date": "2025-11-30",
"description": null,
"request_date": "2025-11-27 16:51:54",
"workflow_state": "Pending approval",
"reason_for_rejection": null,
"status": "Chờ phê duyệt",
"status_color": "Warning",
"is_allow_modify": true,
"is_allow_cancel": true,
"files_list": [
{
"name": "0068d2403c",
"file_url": "https://land.dbiz.com/private/files/image_picker_32BD79E6-7A71-448E-A5DF-6DA7D12A1303-66894-000015E4259DBB5B.png"
}
]
}
}
}

View File

@@ -272,31 +272,71 @@ class ApiConstants {
static const String getPaymentDetails = '/payments'; static const String getPaymentDetails = '/payments';
// ============================================================================ // ============================================================================
// Project Endpoints // Project Endpoints (Frappe ERPNext)
// ============================================================================ // ============================================================================
/// Create new project /// Get project status list (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.project.get_project_status_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: { "message": [{ "status": "...", "label": "...", "color": "...", "index": 0 }] }
static const String getProjectStatusList =
'/building_material.building_material.api.project.get_project_status_list';
/// Get list of project submissions (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.project.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: { "message": [{ "name": "...", "designed_area": "...", "design_area": 0, ... }] }
static const String getProjectList =
'/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';
/// Get project detail (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.project.get_detail
/// Body: { "name": "#DA00011" }
/// Returns: Full project detail with all fields
static const String getProjectDetail =
'/building_material.building_material.api.project.get_detail';
/// Create new project (legacy endpoint - may be deprecated)
/// POST /projects /// POST /projects
static const String createProject = '/projects'; static const String createProject = '/projects';
/// Get user's projects /// Get user's projects (legacy endpoint - may be deprecated)
/// GET /projects?status={status}&page={page}&limit={limit} /// GET /projects?status={status}&page={page}&limit={limit}
static const String getProjects = '/projects'; static const String getProjects = '/projects';
/// Get project details by ID /// Get project details by ID (legacy endpoint - may be deprecated)
/// GET /projects/{projectId} /// GET /projects/{projectId}
static const String getProjectDetails = '/projects'; static const String getProjectDetails = '/projects';
/// Update project /// Update project (legacy endpoint - may be deprecated)
/// PUT /projects/{projectId} /// PUT /projects/{projectId}
static const String updateProject = '/projects'; static const String updateProject = '/projects';
/// Update project progress /// Update project progress (legacy endpoint - may be deprecated)
/// PATCH /projects/{projectId}/progress /// PATCH /projects/{projectId}/progress
/// Body: { "progress": 75 } /// Body: { "progress": 75 }
static const String updateProjectProgress = '/projects'; static const String updateProjectProgress = '/projects';
/// Delete project /// Delete project (legacy endpoint - may be deprecated)
/// DELETE /projects/{projectId} /// DELETE /projects/{projectId}
static const String deleteProject = '/projects'; static const String deleteProject = '/projects';

View File

@@ -64,6 +64,12 @@ class HiveBoxNames {
/// Order status list cache /// Order status list cache
static const String orderStatusBox = 'order_status_box'; static const String orderStatusBox = 'order_status_box';
/// Project status list cache
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,
@@ -77,6 +83,8 @@ class HiveBoxNames {
cityBox, cityBox,
wardBox, wardBox,
orderStatusBox, orderStatusBox,
projectStatusBox,
projectProgressBox,
settingsBox, settingsBox,
cacheBox, cacheBox,
syncStateBox, syncStateBox,
@@ -139,6 +147,8 @@ class HiveTypeIds {
static const int cityModel = 31; static const int cityModel = 31;
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 projectProgressModel = 64;
// Enums (33-61) // Enums (33-61)
static const int userRole = 33; static const int userRole = 33;
@@ -239,6 +249,37 @@ class OrderStatusIndex {
static const int cancelled = 6; static const int cancelled = 6;
} }
/// Project Status Indices
///
/// Index values for project statuses stored in Hive.
/// These correspond to the index field in ProjectStatusModel.
///
/// API Response Structure:
/// - status: "Pending approval" (English status name)
/// - label: "Chờ phê duyệt" (Vietnamese display label)
/// - color: "Warning" (Status color indicator)
/// - index: 1 (Unique identifier)
class ProjectStatusIndex {
// Private constructor to prevent instantiation
ProjectStatusIndex._();
/// Pending approval - "Chờ phê duyệt"
/// Color: Warning
static const int pendingApproval = 1;
/// Approved - "Đã được phê duyệt"
/// Color: Success
static const int approved = 2;
/// Rejected - "Từ chối"
/// Color: Danger
static const int rejected = 3;
/// Cancelled - "HỦY BỎ"
/// Color: Danger
static const int cancelled = 4;
}
/// Hive Keys (continued) /// Hive Keys (continued)
extension HiveKeysContinued on HiveKeys { extension HiveKeysContinued on HiveKeys {
// Cache Box Keys // Cache Box Keys

View File

@@ -102,9 +102,18 @@ class HiveService {
debugPrint( debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatus) ? "" : ""} OrderStatus adapter', 'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatus) ? "" : ""} OrderStatus adapter',
); );
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatusModel) ? "" : ""} OrderStatusModel adapter',
);
debugPrint( debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectType) ? "" : ""} ProjectType adapter', 'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectType) ? "" : ""} ProjectType adapter',
); );
debugPrint(
'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',
); );
@@ -171,6 +180,12 @@ class HiveService {
// Order status box (non-sensitive) - caches order status list from API // Order status box (non-sensitive) - caches order status list from API
Hive.openBox<dynamic>(HiveBoxNames.orderStatusBox), Hive.openBox<dynamic>(HiveBoxNames.orderStatusBox),
// Project status box (non-sensitive) - caches project status list from API
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)

View File

@@ -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,

View File

@@ -42,6 +42,7 @@ import 'package:worker/features/price_policy/price_policy.dart';
import 'package:worker/features/products/presentation/pages/product_detail_page.dart'; import 'package:worker/features/products/presentation/pages/product_detail_page.dart';
import 'package:worker/features/products/presentation/pages/products_page.dart'; import 'package:worker/features/products/presentation/pages/products_page.dart';
import 'package:worker/features/products/presentation/pages/write_review_page.dart'; import 'package:worker/features/products/presentation/pages/write_review_page.dart';
import 'package:worker/features/projects/domain/entities/project_submission.dart';
import 'package:worker/features/projects/presentation/pages/submission_create_page.dart'; import 'package:worker/features/projects/presentation/pages/submission_create_page.dart';
import 'package:worker/features/projects/presentation/pages/submissions_page.dart'; import 'package:worker/features/projects/presentation/pages/submissions_page.dart';
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart'; import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
@@ -391,12 +392,17 @@ final routerProvider = Provider<GoRouter>((ref) {
MaterialPage(key: state.pageKey, child: const SubmissionsPage()), MaterialPage(key: state.pageKey, child: const SubmissionsPage()),
), ),
// Submission Create Route // Submission Create/Edit Route
GoRoute( GoRoute(
path: RouteNames.submissionCreate, path: RouteNames.submissionCreate,
name: RouteNames.submissionCreate, name: RouteNames.submissionCreate,
pageBuilder: (context, state) => pageBuilder: (context, state) {
MaterialPage(key: state.pageKey, child: const SubmissionCreatePage()), final submission = state.extra as ProjectSubmission?;
return MaterialPage(
key: state.pageKey,
child: SubmissionCreatePage(submission: submission),
);
},
), ),
// Quotes Route // Quotes Route

View File

@@ -272,7 +272,7 @@ final class AuthProvider extends $AsyncNotifierProvider<Auth, User?> {
Auth create() => Auth(); Auth create() => Auth();
} }
String _$authHash() => r'f1a16022d628a21f230c0bb567e80ff6e293d840'; String _$authHash() => r'f0438cf6eb9eb17c0afc6b23055acd09926b21ae';
/// Authentication Provider /// Authentication Provider
/// ///

View File

@@ -239,18 +239,4 @@ class _HomePageState extends ConsumerState<HomePage> {
), ),
); );
} }
/// Show coming soon message
void _showComingSoon(
BuildContext context,
String feature,
AppLocalizations l10n,
) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$feature - ${l10n.comingSoon}'),
duration: const Duration(seconds: 1),
),
);
}
} }

View File

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

View File

@@ -0,0 +1,47 @@
/// Project Status Local Data Source
///
/// Handles local caching of project status 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_status_model.dart';
/// Project Status Local Data Source
class ProjectStatusLocalDataSource {
/// Get Hive box for project statuses
Box<dynamic> get _box => Hive.box(HiveBoxNames.projectStatusBox);
/// Save project status list to cache
Future<void> cacheStatusList(List<ProjectStatusModel> statuses) async {
// Clear existing cache
await _box.clear();
// Save each status with its index as key
for (final status in statuses) {
await _box.put(status.index, status);
}
}
/// Get cached project status list
List<ProjectStatusModel> getCachedStatusList() {
try {
final values = _box.values.whereType<ProjectStatusModel>().toList()
// Sort by index
..sort((a, b) => a.index.compareTo(b.index));
return values;
} catch (e) {
return [];
}
}
/// Check if cache exists and is not empty
bool hasCachedData() {
return _box.isNotEmpty;
}
/// Clear all cached statuses
Future<void> clearCache() async {
await _box.clear();
}
}

View File

@@ -3,166 +3,304 @@
/// Handles remote API calls for project submissions. /// Handles remote API calls for project submissions.
library; library;
import 'package:worker/features/projects/domain/entities/project_submission.dart'; 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 /// Submissions Remote Data Source
/// ///
/// Abstract interface for remote submissions operations. /// Interface for remote project submission operations.
abstract class SubmissionsRemoteDataSource { 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 /// Fetch all submissions from remote API
Future<List<ProjectSubmission>> getSubmissions(); Future<List<ProjectSubmissionModel>> getSubmissions({
int limitStart = 0,
int limitPageLength = 0,
});
/// Fetch a single submission by ID /// Fetch project detail by name
Future<ProjectSubmission> getSubmissionById(String submissionId); /// Returns the full project detail as a model
Future<ProjectSubmissionModel> getSubmissionDetail(String name);
/// Create a new submission /// Create or update a project submission
Future<ProjectSubmission> createSubmission(ProjectSubmission submission); /// Returns the project name (ID) from the API response
Future<String> saveSubmission(ProjectSubmissionRequest request);
/// Update an existing submission /// Upload a file for a project submission
Future<ProjectSubmission> updateSubmission(ProjectSubmission submission); /// [projectName] is the project ID returned from saveSubmission
/// [filePath] is the local path to the file
/// Delete a submission /// Returns the uploaded file URL
Future<void> deleteSubmission(String submissionId); Future<String> uploadProjectFile({
required String projectName,
required String filePath,
});
} }
/// Mock Implementation of Submissions Remote Data Source /// Submissions Remote Data Source Implementation
/// ///
/// Provides mock data for development and testing. /// Uses Frappe API endpoints for project submissions.
class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource { class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource {
const SubmissionsRemoteDataSourceImpl(this._dioClient);
final DioClient _dioClient;
/// Get project status list
///
/// Calls: POST /api/method/building_material.building_material.api.project.get_project_status_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: List of project statuses with labels and colors
@override @override
Future<List<ProjectSubmission>> getSubmissions() async { Future<List<ProjectStatusModel>> getProjectStatusList() async {
// Simulate network delay try {
await Future<void>.delayed(const Duration(milliseconds: 500)); final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getProjectStatusList}',
return [ data: <String, dynamic>{
ProjectSubmission( 'limit_start': 0,
submissionId: 'DA001', 'limit_page_length': 0,
userId: 'user123', },
projectName: 'Chung cư Vinhomes Grand Park - Block A1',
projectAddress: 'TP.HCM',
projectValue: 850000000,
projectType: ProjectType.residential,
status: SubmissionStatus.approved,
beforePhotos: [],
afterPhotos: [],
invoices: [],
submittedAt: DateTime(2023, 11, 15),
reviewedAt: DateTime(2023, 11, 20),
pointsEarned: 8500,
),
ProjectSubmission(
submissionId: 'DA002',
userId: 'user123',
projectName: 'Trung tâm thương mại Bitexco',
projectAddress: 'TP.HCM',
projectValue: 2200000000,
projectType: ProjectType.commercial,
status: SubmissionStatus.pending,
beforePhotos: [],
afterPhotos: [],
invoices: [],
submittedAt: DateTime(2023, 11, 25),
),
ProjectSubmission(
submissionId: 'DA003',
userId: 'user123',
projectName: 'Biệt thự sinh thái Ecopark',
projectAddress: 'Hà Nội',
projectValue: 420000000,
projectType: ProjectType.residential,
status: SubmissionStatus.approved,
beforePhotos: [],
afterPhotos: [],
invoices: [],
submittedAt: DateTime(2023, 10, 10),
reviewedAt: DateTime(2023, 10, 15),
pointsEarned: 4200,
),
ProjectSubmission(
submissionId: 'DA004',
userId: 'user123',
projectName: 'Nhà xưởng sản xuất ABC',
projectAddress: 'Bình Dương',
projectValue: 1500000000,
projectType: ProjectType.industrial,
status: SubmissionStatus.rejected,
beforePhotos: [],
afterPhotos: [],
invoices: [],
submittedAt: DateTime(2023, 11, 20),
reviewedAt: DateTime(2023, 11, 28),
rejectionReason: 'Thiếu giấy phép xây dựng và báo cáo tác động môi trường',
),
ProjectSubmission(
submissionId: 'DA005',
userId: 'user123',
projectName: 'Khách sạn 5 sao Diamond Plaza',
projectAddress: 'Đà Nẵng',
projectValue: 5800000000,
projectType: ProjectType.commercial,
status: SubmissionStatus.pending,
beforePhotos: [],
afterPhotos: [],
invoices: [],
submittedAt: DateTime(2023, 12, 1),
),
ProjectSubmission(
submissionId: 'DA006',
userId: 'user123',
projectName: 'Khu đô thị thông minh Smart City',
projectAddress: 'Hà Nội',
projectValue: 8500000000,
projectType: ProjectType.residential,
status: SubmissionStatus.approved,
beforePhotos: [],
afterPhotos: [],
invoices: [],
submittedAt: DateTime(2023, 11, 10),
reviewedAt: DateTime(2023, 11, 18),
pointsEarned: 85000,
),
];
}
@override
Future<ProjectSubmission> getSubmissionById(String submissionId) async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 300));
final submissions = await getSubmissions();
return submissions.firstWhere(
(s) => s.submissionId == submissionId,
orElse: () => throw Exception('Submission not found'),
); );
final data = response.data;
if (data == null) {
throw Exception('No data received from getProjectStatusList API');
} }
@override // API returns: { "message": [...] }
Future<ProjectSubmission> createSubmission( final message = data['message'];
ProjectSubmission submission, if (message == null) {
) async { throw Exception('No message field in getProjectStatusList response');
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 800));
// In real implementation, this would call the API
return submission;
} }
@override final List<dynamic> statusList = message as List<dynamic>;
Future<ProjectSubmission> updateSubmission( return statusList
ProjectSubmission submission, .map((json) =>
) async { ProjectStatusModel.fromJson(json as Map<String, dynamic>))
// Simulate network delay .toList();
await Future<void>.delayed(const Duration(milliseconds: 600)); } catch (e) {
throw Exception('Failed to get project status list: $e');
// In real implementation, this would call the API }
return submission;
} }
/// 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 @override
Future<void> deleteSubmission(String submissionId) async { Future<List<ProjectProgressModel>> getProjectProgressList() async {
// Simulate network delay try {
await Future<void>.delayed(const Duration(milliseconds: 400)); 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,
},
);
// In real implementation, this would call the API 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
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: List of project submissions
@override
Future<List<ProjectSubmissionModel>> getSubmissions({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getProjectList}',
data: {
'limit_start': limitStart,
'limit_page_length': limitPageLength,
},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getProjectList API');
}
// API returns: { "message": [...] }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getProjectList response');
}
final List<dynamic> submissionsList = message as List<dynamic>;
return submissionsList
.map((json) =>
ProjectSubmissionModel.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
throw Exception('Failed to get project submissions: $e');
}
}
/// Get project detail by name
///
/// Calls: POST /api/method/building_material.building_material.api.project.get_detail
/// Body: { "name": "#DA00011" }
/// Response: { "message": { "success": true, "data": {...} } }
/// Returns: Full project detail as model
@override
Future<ProjectSubmissionModel> getSubmissionDetail(String name) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getProjectDetail}',
data: {'name': name},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getProjectDetail API');
}
// API returns: { "message": { "success": true, "data": {...} } }
final message = data['message'] as Map<String, dynamic>?;
if (message == null) {
throw Exception('No message field in getProjectDetail response');
}
final detailData = message['data'] as Map<String, dynamic>?;
if (detailData == null) {
throw Exception('No data field in getProjectDetail response');
}
return ProjectSubmissionModel.fromJson(detailData);
} catch (e) {
throw Exception('Failed to get project detail: $e');
}
}
/// Save (create/update) a project submission
///
/// Calls: POST /api/method/building_material.building_material.api.project.save
/// 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': '0',
'folder': 'Home/Attachments',
'doctype': 'Architectural Project',
'docname': projectName,
'optimize': 'true',
});
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.uploadFile}',
data: formData,
);
final data = response.data;
if (data == null) {
throw Exception('No data received from uploadFile API');
}
// Check for error in response
if (data['exc_type'] != null || data['exception'] != null) {
final errorMessage =
data['_server_messages'] ?? data['exception'] ?? 'Unknown error';
throw Exception('API error: $errorMessage');
}
// Extract file URL from response
// Response format: { "message": { "file_url": "/files/...", ... } }
final message = data['message'];
if (message == null || message['file_url'] == null) {
throw Exception('No file URL in uploadFile response');
}
return message['file_url'] as String;
} catch (e) {
throw Exception('Failed to upload project file: $e');
}
} }
} }

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
/// Project Status Model
///
/// Data model for project status from API responses with Hive caching.
library;
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/features/projects/domain/entities/project_status.dart';
part 'project_status_model.g.dart';
/// Project Status Model - Type ID: 63
@HiveType(typeId: HiveTypeIds.projectStatusModel)
class ProjectStatusModel extends HiveObject {
@HiveField(0)
final String status;
@HiveField(1)
final String label;
@HiveField(2)
final String color;
@HiveField(3)
final int index;
ProjectStatusModel({
required this.status,
required this.label,
required this.color,
required this.index,
});
/// Create from JSON
factory ProjectStatusModel.fromJson(Map<String, dynamic> json) {
return ProjectStatusModel(
status: json['status'] as String,
label: json['label'] as String,
color: json['color'] as String,
index: json['index'] as int,
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'status': status,
'label': label,
'color': color,
'index': index,
};
}
/// Convert to entity
ProjectStatus toEntity() {
return ProjectStatus(
status: status,
label: label,
color: color,
index: index,
);
}
/// Create from entity
factory ProjectStatusModel.fromEntity(ProjectStatus entity) {
return ProjectStatusModel(
status: entity.status,
label: entity.label,
color: entity.color,
index: entity.index,
);
}
}

View File

@@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'project_status_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ProjectStatusModelAdapter extends TypeAdapter<ProjectStatusModel> {
@override
final typeId = 63;
@override
ProjectStatusModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ProjectStatusModel(
status: fields[0] as String,
label: fields[1] as String,
color: fields[2] as String,
index: (fields[3] as num).toInt(),
);
}
@override
void write(BinaryWriter writer, ProjectStatusModel obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.status)
..writeByte(1)
..write(obj.label)
..writeByte(2)
..write(obj.color)
..writeByte(3)
..write(obj.index);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ProjectStatusModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -1,129 +1,255 @@
import 'dart:convert'; /// Project Submission Model
import 'package:hive_ce/hive.dart'; ///
import 'package:worker/core/constants/storage_constants.dart'; /// Data model for project submission from API responses.
import 'package:worker/core/database/models/enums.dart'; /// Based on API response from building_material.building_material.api.project.get_detail
library;
part 'project_submission_model.g.dart'; import 'package:worker/features/projects/domain/entities/project_submission.dart';
@HiveType(typeId: HiveTypeIds.projectSubmissionModel) /// Project File Model
class ProjectSubmissionModel extends HiveObject { class ProjectFileModel {
ProjectSubmissionModel({ /// Unique file identifier (API: name)
required this.submissionId, final String id;
required this.userId,
required this.projectName, /// Full URL to the file (API: file_url)
required this.projectAddress, final String fileUrl;
required this.projectValue,
required this.projectType, const ProjectFileModel({
this.beforePhotos, required this.id,
this.afterPhotos, required this.fileUrl,
this.invoices,
required this.status,
this.reviewNotes,
this.rejectionReason,
this.pointsEarned,
required this.submittedAt,
this.reviewedAt,
this.reviewedBy,
}); });
@HiveField(0) /// Create from JSON (API response)
final String submissionId; factory ProjectFileModel.fromJson(Map<String, dynamic> json) {
@HiveField(1) return ProjectFileModel(
final String userId; id: json['name'] as String,
@HiveField(2) fileUrl: json['file_url'] as String,
final String projectName;
@HiveField(3)
final String projectAddress;
@HiveField(4)
final double projectValue;
@HiveField(5)
final ProjectType projectType;
@HiveField(6)
final String? beforePhotos;
@HiveField(7)
final String? afterPhotos;
@HiveField(8)
final String? invoices;
@HiveField(9)
final SubmissionStatus status;
@HiveField(10)
final String? reviewNotes;
@HiveField(11)
final String? rejectionReason;
@HiveField(12)
final int? pointsEarned;
@HiveField(13)
final DateTime submittedAt;
@HiveField(14)
final DateTime? reviewedAt;
@HiveField(15)
final String? reviewedBy;
factory ProjectSubmissionModel.fromJson(
Map<String, dynamic> json,
) => ProjectSubmissionModel(
submissionId: json['submission_id'] as String,
userId: json['user_id'] as String,
projectName: json['project_name'] as String,
projectAddress: json['project_address'] as String,
projectValue: (json['project_value'] as num).toDouble(),
projectType: ProjectType.values.firstWhere(
(e) => e.name == json['project_type'],
),
beforePhotos: json['before_photos'] != null
? jsonEncode(json['before_photos'])
: null,
afterPhotos: json['after_photos'] != null
? jsonEncode(json['after_photos'])
: null,
invoices: json['invoices'] != null ? jsonEncode(json['invoices']) : null,
status: SubmissionStatus.values.firstWhere((e) => e.name == json['status']),
reviewNotes: json['review_notes'] as String?,
rejectionReason: json['rejection_reason'] as String?,
pointsEarned: json['points_earned'] as int?,
submittedAt: DateTime.parse(json['submitted_at']?.toString() ?? ''),
reviewedAt: json['reviewed_at'] != null
? DateTime.parse(json['reviewed_at']?.toString() ?? '')
: null,
reviewedBy: json['reviewed_by'] as String?,
); );
}
Map<String, dynamic> toJson() => { /// Convert to JSON
'submission_id': submissionId, Map<String, dynamic> toJson() {
'user_id': userId, return {
'project_name': projectName, 'name': id,
'project_address': projectAddress, 'file_url': fileUrl,
'project_value': projectValue,
'project_type': projectType.name,
'before_photos': beforePhotos != null ? jsonDecode(beforePhotos!) : null,
'after_photos': afterPhotos != null ? jsonDecode(afterPhotos!) : null,
'invoices': invoices != null ? jsonDecode(invoices!) : null,
'status': status.name,
'review_notes': reviewNotes,
'rejection_reason': rejectionReason,
'points_earned': pointsEarned,
'submitted_at': submittedAt.toIso8601String(),
'reviewed_at': reviewedAt?.toIso8601String(),
'reviewed_by': reviewedBy,
}; };
List<String>? get beforePhotosList {
if (beforePhotos == null) return null;
try {
final decoded = jsonDecode(beforePhotos!) as List;
return decoded.map((e) => e.toString()).toList();
} catch (e) {
return null;
}
} }
List<String>? get afterPhotosList { /// Convert to entity
if (afterPhotos == null) return null; ProjectFile toEntity() {
try { return ProjectFile(
final decoded = jsonDecode(afterPhotos!) as List; id: id,
return decoded.map((e) => e.toString()).toList(); fileUrl: fileUrl,
} catch (e) { );
return null;
} }
/// Create from entity
factory ProjectFileModel.fromEntity(ProjectFile entity) {
return ProjectFileModel(
id: entity.id,
fileUrl: entity.fileUrl,
);
}
}
/// Project Submission Model
class ProjectSubmissionModel {
/// Unique submission identifier (API: name)
final String submissionId;
/// Project name/title (API: designed_area)
final String designedArea;
/// Design area value in square meters (API: design_area)
final double designArea;
/// Submission/request date (API: request_date)
final DateTime requestDate;
/// Status label - Vietnamese (API: status)
final String status;
/// Rejection reason if rejected (API: reason_for_rejection)
final String? reasonForRejection;
/// Status color indicator (API: status_color)
final String statusColor;
/// Project address (API: address_of_project)
final String? addressOfProject;
/// Project owner name (API: project_owner)
final String? projectOwner;
/// Design firm name (API: design_firm)
final String? designFirm;
/// Construction contractor name (API: contruction_contractor)
final String? constructionContractor;
/// Products included in the design (API: products_included_in_the_design)
final String? productsIncludedInTheDesign;
/// Project progress ID reference (API: project_progress)
final String? projectProgress;
/// Expected commencement date (API: expected_commencement_date)
final DateTime? expectedCommencementDate;
/// Project description (API: description)
final String? description;
/// Workflow state (API: workflow_state)
final String? workflowState;
/// Whether the submission can be modified (API: is_allow_modify)
final bool isAllowModify;
/// Whether the submission can be cancelled (API: is_allow_cancel)
final bool isAllowCancel;
/// List of attached files (API: files_list)
final List<ProjectFileModel> filesList;
const ProjectSubmissionModel({
required this.submissionId,
required this.designedArea,
required this.designArea,
required this.requestDate,
required this.status,
this.reasonForRejection,
required this.statusColor,
this.addressOfProject,
this.projectOwner,
this.designFirm,
this.constructionContractor,
this.productsIncludedInTheDesign,
this.projectProgress,
this.expectedCommencementDate,
this.description,
this.workflowState,
this.isAllowModify = false,
this.isAllowCancel = false,
this.filesList = const [],
});
/// Create from JSON (API response)
/// Handles both list response and detail response formats
factory ProjectSubmissionModel.fromJson(Map<String, dynamic> json) {
// Parse expected_commencement_date
DateTime? expectedDate;
final expectedDateStr = json['expected_commencement_date'] as String?;
if (expectedDateStr != null && expectedDateStr.isNotEmpty) {
try {
expectedDate = DateTime.parse(expectedDateStr);
} catch (_) {}
}
// Parse files_list
final filesListJson = json['files_list'] as List<dynamic>?;
final filesList = filesListJson
?.map((f) => ProjectFileModel.fromJson(f as Map<String, dynamic>))
.toList() ??
[];
return ProjectSubmissionModel(
submissionId: json['name'] as String,
designedArea: json['designed_area'] as String,
designArea: (json['design_area'] as num).toDouble(),
requestDate: DateTime.parse(json['request_date'] as String),
status: json['status'] as String,
reasonForRejection: json['reason_for_rejection'] as String?,
statusColor: json['status_color'] as String,
addressOfProject: json['address_of_project'] as String?,
projectOwner: json['project_owner'] as String?,
designFirm: json['design_firm'] as String?,
constructionContractor: json['contruction_contractor'] as String?,
productsIncludedInTheDesign:
json['products_included_in_the_design'] as String?,
projectProgress: json['project_progress'] as String?,
expectedCommencementDate: expectedDate,
description: json['description'] as String?,
workflowState: json['workflow_state'] as String?,
isAllowModify: json['is_allow_modify'] as bool? ?? false,
isAllowCancel: json['is_allow_cancel'] as bool? ?? false,
filesList: filesList,
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'name': submissionId,
'designed_area': designedArea,
'design_area': designArea,
'request_date': requestDate.toIso8601String(),
'status': status,
'reason_for_rejection': reasonForRejection,
'status_color': statusColor,
'address_of_project': addressOfProject,
'project_owner': projectOwner,
'design_firm': designFirm,
'contruction_contractor': constructionContractor,
'products_included_in_the_design': productsIncludedInTheDesign,
'project_progress': projectProgress,
'expected_commencement_date':
expectedCommencementDate?.toIso8601String(),
'description': description,
'workflow_state': workflowState,
'is_allow_modify': isAllowModify,
'is_allow_cancel': isAllowCancel,
'files_list': filesList.map((f) => f.toJson()).toList(),
};
}
/// Convert to entity
ProjectSubmission toEntity() {
return ProjectSubmission(
submissionId: submissionId,
designedArea: designedArea,
designArea: designArea,
requestDate: requestDate,
status: status,
reasonForRejection: reasonForRejection,
statusColor: statusColor,
addressOfProject: addressOfProject,
projectOwner: projectOwner,
designFirm: designFirm,
constructionContractor: constructionContractor,
productsIncludedInTheDesign: productsIncludedInTheDesign,
projectProgress: projectProgress,
expectedCommencementDate: expectedCommencementDate,
description: description,
workflowState: workflowState,
isAllowModify: isAllowModify,
isAllowCancel: isAllowCancel,
filesList: filesList.map((f) => f.toEntity()).toList(),
);
}
/// Create from entity
factory ProjectSubmissionModel.fromEntity(ProjectSubmission entity) {
return ProjectSubmissionModel(
submissionId: entity.submissionId,
designedArea: entity.designedArea,
designArea: entity.designArea,
requestDate: entity.requestDate,
status: entity.status,
reasonForRejection: entity.reasonForRejection,
statusColor: entity.statusColor,
addressOfProject: entity.addressOfProject,
projectOwner: entity.projectOwner,
designFirm: entity.designFirm,
constructionContractor: entity.constructionContractor,
productsIncludedInTheDesign: entity.productsIncludedInTheDesign,
projectProgress: entity.projectProgress,
expectedCommencementDate: entity.expectedCommencementDate,
description: entity.description,
workflowState: entity.workflowState,
isAllowModify: entity.isAllowModify,
isAllowCancel: entity.isAllowCancel,
filesList:
entity.filesList.map((f) => ProjectFileModel.fromEntity(f)).toList(),
);
} }
} }

View File

@@ -1,87 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'project_submission_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ProjectSubmissionModelAdapter
extends TypeAdapter<ProjectSubmissionModel> {
@override
final typeId = 14;
@override
ProjectSubmissionModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ProjectSubmissionModel(
submissionId: fields[0] as String,
userId: fields[1] as String,
projectName: fields[2] as String,
projectAddress: fields[3] as String,
projectValue: (fields[4] as num).toDouble(),
projectType: fields[5] as ProjectType,
beforePhotos: fields[6] as String?,
afterPhotos: fields[7] as String?,
invoices: fields[8] as String?,
status: fields[9] as SubmissionStatus,
reviewNotes: fields[10] as String?,
rejectionReason: fields[11] as String?,
pointsEarned: (fields[12] as num?)?.toInt(),
submittedAt: fields[13] as DateTime,
reviewedAt: fields[14] as DateTime?,
reviewedBy: fields[15] as String?,
);
}
@override
void write(BinaryWriter writer, ProjectSubmissionModel obj) {
writer
..writeByte(16)
..writeByte(0)
..write(obj.submissionId)
..writeByte(1)
..write(obj.userId)
..writeByte(2)
..write(obj.projectName)
..writeByte(3)
..write(obj.projectAddress)
..writeByte(4)
..write(obj.projectValue)
..writeByte(5)
..write(obj.projectType)
..writeByte(6)
..write(obj.beforePhotos)
..writeByte(7)
..write(obj.afterPhotos)
..writeByte(8)
..write(obj.invoices)
..writeByte(9)
..write(obj.status)
..writeByte(10)
..write(obj.reviewNotes)
..writeByte(11)
..write(obj.rejectionReason)
..writeByte(12)
..write(obj.pointsEarned)
..writeByte(13)
..write(obj.submittedAt)
..writeByte(14)
..write(obj.reviewedAt)
..writeByte(15)
..write(obj.reviewedBy);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ProjectSubmissionModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

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

View File

@@ -1,66 +1,172 @@
/// Submissions Repository Implementation /// Submissions Repository Implementation
/// ///
/// Implements the submissions repository interface. /// 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/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_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';
/// Submissions Repository Implementation /// Submissions Repository Implementation
/// ///
/// Handles data operations for project submissions. /// Handles data operations for project submissions with cache-first pattern.
class SubmissionsRepositoryImpl implements SubmissionsRepository { class SubmissionsRepositoryImpl implements SubmissionsRepository {
const SubmissionsRepositoryImpl(
this._remoteDataSource,
this._statusLocalDataSource,
this._progressLocalDataSource,
);
const SubmissionsRepositoryImpl(this._remoteDataSource);
final SubmissionsRemoteDataSource _remoteDataSource; final SubmissionsRemoteDataSource _remoteDataSource;
final ProjectStatusLocalDataSource _statusLocalDataSource;
final ProjectProgressLocalDataSource _progressLocalDataSource;
/// Get project status 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 @override
Future<List<ProjectSubmission>> getSubmissions() async { Future<List<ProjectStatus>> getProjectStatusList({
bool forceRefresh = false,
}) async {
// Check cache first (unless force refresh)
if (!forceRefresh && _statusLocalDataSource.hasCachedData()) {
final cachedStatuses = _statusLocalDataSource.getCachedStatusList();
if (cachedStatuses.isNotEmpty) {
// Return cached data immediately
// Also refresh cache in background (fire and forget)
_refreshStatusCache();
return cachedStatuses.map((model) => model.toEntity()).toList();
}
}
// No cache or force refresh - fetch from API
try { try {
return await _remoteDataSource.getSubmissions(); final statusModels = await _remoteDataSource.getProjectStatusList();
// Cache the result
await _statusLocalDataSource.cacheStatusList(statusModels);
return statusModels.map((model) => model.toEntity()).toList();
} catch (e) { } catch (e) {
// In real implementation, handle errors properly // If API fails, try to return cached data as fallback
// For now, rethrow final cachedStatuses = _statusLocalDataSource.getCachedStatusList();
if (cachedStatuses.isNotEmpty) {
return cachedStatuses.map((model) => model.toEntity()).toList();
}
rethrow; rethrow;
} }
} }
@override /// Refresh status cache in background
Future<ProjectSubmission> getSubmissionById(String submissionId) async { Future<void> _refreshStatusCache() async {
try { try {
return await _remoteDataSource.getSubmissionById(submissionId); final statusModels = await _remoteDataSource.getProjectStatusList();
await _statusLocalDataSource.cacheStatusList(statusModels);
} catch (e) {
// Silently fail - we already returned cached data
}
}
/// 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,
int limitPageLength = 0,
}) async {
try {
final submissionModels = await _remoteDataSource.getSubmissions(
limitStart: limitStart,
limitPageLength: limitPageLength,
);
return submissionModels.map((model) => model.toEntity()).toList();
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
@override @override
Future<ProjectSubmission> createSubmission( Future<ProjectSubmission> getSubmissionDetail(String name) async {
ProjectSubmission submission,
) async {
try { try {
return await _remoteDataSource.createSubmission(submission); final model = await _remoteDataSource.getSubmissionDetail(name);
return model.toEntity();
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
@override @override
Future<ProjectSubmission> updateSubmission( Future<String> saveSubmission(ProjectSubmissionRequest request) async {
ProjectSubmission submission,
) async {
try { try {
return await _remoteDataSource.updateSubmission(submission); return await _remoteDataSource.saveSubmission(request);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
@override @override
Future<void> deleteSubmission(String submissionId) async { Future<String> uploadProjectFile({
required String projectName,
required String filePath,
}) async {
try { try {
await _remoteDataSource.deleteSubmission(submissionId); return await _remoteDataSource.uploadProjectFile(
projectName: projectName,
filePath: filePath,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }

View File

@@ -3,7 +3,7 @@
/// Represents a request for design consultation service. /// Represents a request for design consultation service.
library; library;
import 'project_submission.dart'; import 'package:worker/features/projects/domain/entities/project_type.dart';
/// Design status enum /// Design status enum
enum DesignStatus { enum DesignStatus {

View 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)';
}

View File

@@ -0,0 +1,33 @@
/// Project Status Entity
///
/// Represents a project status option from the API.
library;
import 'package:equatable/equatable.dart';
/// Project Status Entity
///
/// Similar to OrderStatus - represents status options for project submissions.
class ProjectStatus extends Equatable {
/// Status value (e.g., "Pending approval", "Approved", "Rejected", "Cancelled")
final String status;
/// Vietnamese label (e.g., "Chờ phê duyệt", "Đã được phê duyệt", "Từ chối", "HỦY BỎ")
final String label;
/// Color indicator (e.g., "Warning", "Success", "Danger")
final String color;
/// Display order index
final int index;
const ProjectStatus({
required this.status,
required this.label,
required this.color,
required this.index,
});
@override
List<Object?> get props => [status, label, color, index];
}

View File

@@ -1,242 +1,216 @@
/// Domain Entity: Project Submission /// Domain Entity: Project Submission
/// ///
/// Represents a completed project submitted for loyalty points. /// Represents a completed project submitted for loyalty points.
/// Based on API response from building_material.building_material.api.project.get_detail
library; library;
/// Project type enum import 'package:equatable/equatable.dart';
enum ProjectType {
/// Residential project
residential,
/// Commercial project /// Project File Entity
commercial, ///
/// Represents an uploaded file attached to a project submission.
class ProjectFile extends Equatable {
/// Unique file identifier (API: name)
final String id;
/// Industrial project /// Full URL to the file (API: file_url)
industrial, final String fileUrl;
/// Public infrastructure const ProjectFile({
infrastructure, required this.id,
required this.fileUrl,
});
/// Other type @override
other; List<Object?> get props => [id, fileUrl];
/// Get display name for project type
String get displayName {
switch (this) {
case ProjectType.residential:
return 'Residential';
case ProjectType.commercial:
return 'Commercial';
case ProjectType.industrial:
return 'Industrial';
case ProjectType.infrastructure:
return 'Infrastructure';
case ProjectType.other:
return 'Other';
}
}
}
/// Submission status enum
enum SubmissionStatus {
/// Submitted, pending review
pending,
/// Under review
reviewing,
/// Approved, points awarded
approved,
/// Rejected
rejected;
/// Get display name for status
String get displayName {
switch (this) {
case SubmissionStatus.pending:
return 'Pending';
case SubmissionStatus.reviewing:
return 'Reviewing';
case SubmissionStatus.approved:
return 'Approved';
case SubmissionStatus.rejected:
return 'Rejected';
}
}
} }
/// Project Submission Entity /// Project Submission Entity
/// ///
/// Contains information about a completed project: /// Contains information about a completed project submission.
/// - Project details /// Mapped from API response:
/// - Before/after photos /// - name -> submissionId
/// - Invoice documentation /// - designed_area -> designedArea (project name/title)
/// - Review status /// - address_of_project -> addressOfProject
/// - Points earned /// - project_owner -> projectOwner
class ProjectSubmission { /// - design_firm -> designFirm
/// Unique submission identifier /// - contruction_contractor -> constructionContractor
/// - design_area -> designArea (area value in m²)
/// - products_included_in_the_design -> productsIncludedInTheDesign
/// - project_progress -> projectProgress (ID reference)
/// - expected_commencement_date -> expectedCommencementDate
/// - description -> description
/// - request_date -> requestDate
/// - workflow_state -> workflowState
/// - reason_for_rejection -> reasonForRejection
/// - status -> status (Vietnamese label)
/// - status_color -> statusColor
/// - is_allow_modify -> isAllowModify
/// - is_allow_cancel -> isAllowCancel
/// - files_list -> filesList
class ProjectSubmission extends Equatable {
/// Unique submission identifier (API: name)
final String submissionId; final String submissionId;
/// User ID who submitted /// Project name/title (API: designed_area)
final String userId; final String designedArea;
/// Project name /// Project address (API: address_of_project)
final String projectName; final String? addressOfProject;
/// Project address/location /// Project owner name (API: project_owner)
final String? projectAddress; final String? projectOwner;
/// Project value/cost /// Design firm name (API: design_firm)
final double projectValue; final String? designFirm;
/// Project type /// Construction contractor name (API: contruction_contractor)
final ProjectType projectType; final String? constructionContractor;
/// Before photos URLs /// Design area value in square meters (API: design_area)
final List<String> beforePhotos; final double designArea;
/// After photos URLs /// Products included in the design (API: products_included_in_the_design)
final List<String> afterPhotos; final String? productsIncludedInTheDesign;
/// Invoice/receipt URLs /// Project progress ID reference (API: project_progress)
final List<String> invoices; final String? projectProgress;
/// Submission status /// Expected commencement date (API: expected_commencement_date)
final SubmissionStatus status; final DateTime? expectedCommencementDate;
/// Review notes from admin /// Project description (API: description)
final String? reviewNotes; final String? description;
/// Rejection reason (if rejected) /// Submission/request date (API: request_date)
final String? rejectionReason; final DateTime requestDate;
/// Points earned (if approved) /// Workflow state (API: workflow_state)
final int? pointsEarned; /// e.g., "Pending approval", "Approved", "Rejected", "Cancelled"
final String? workflowState;
/// Submission timestamp /// Rejection reason if rejected (API: reason_for_rejection)
final DateTime submittedAt; final String? reasonForRejection;
/// Review timestamp /// Status label - Vietnamese (API: status)
final DateTime? reviewedAt; /// e.g., "Chờ phê duyệt", "Đã được phê duyệt", "Từ chối", "HỦY BỎ"
final String status;
/// ID of admin who reviewed /// Status color indicator (API: status_color)
final String? reviewedBy; /// Values: "Warning", "Success", "Danger"
final String statusColor;
/// Whether the submission can be modified (API: is_allow_modify)
final bool isAllowModify;
/// Whether the submission can be cancelled (API: is_allow_cancel)
final bool isAllowCancel;
/// List of attached files (API: files_list)
final List<ProjectFile> filesList;
const ProjectSubmission({ const ProjectSubmission({
required this.submissionId, required this.submissionId,
required this.userId, required this.designedArea,
required this.projectName, this.addressOfProject,
this.projectAddress, this.projectOwner,
required this.projectValue, this.designFirm,
required this.projectType, this.constructionContractor,
required this.beforePhotos, required this.designArea,
required this.afterPhotos, this.productsIncludedInTheDesign,
required this.invoices, this.projectProgress,
this.expectedCommencementDate,
this.description,
required this.requestDate,
this.workflowState,
this.reasonForRejection,
required this.status, required this.status,
this.reviewNotes, required this.statusColor,
this.rejectionReason, this.isAllowModify = false,
this.pointsEarned, this.isAllowCancel = false,
required this.submittedAt, this.filesList = const [],
this.reviewedAt,
this.reviewedBy,
}); });
/// Check if submission is pending /// Check if submission is pending approval
bool get isPending => status == SubmissionStatus.pending; bool get isPending => statusColor == 'Warning';
/// Check if submission is under review
bool get isReviewing => status == SubmissionStatus.reviewing;
/// Check if submission is approved /// Check if submission is approved
bool get isApproved => status == SubmissionStatus.approved; bool get isApproved => statusColor == 'Success';
/// Check if submission is rejected /// Check if submission is rejected or cancelled
bool get isRejected => status == SubmissionStatus.rejected; bool get isRejected => statusColor == 'Danger';
/// Check if submission has been reviewed
bool get isReviewed =>
status == SubmissionStatus.approved ||
status == SubmissionStatus.rejected;
/// Check if submission has before photos
bool get hasBeforePhotos => beforePhotos.isNotEmpty;
/// Check if submission has after photos
bool get hasAfterPhotos => afterPhotos.isNotEmpty;
/// Check if submission has invoices
bool get hasInvoices => invoices.isNotEmpty;
/// Get total number of photos
int get totalPhotos => beforePhotos.length + afterPhotos.length;
/// Get review duration
Duration? get reviewDuration {
if (reviewedAt == null) return null;
return reviewedAt!.difference(submittedAt);
}
/// Copy with method for immutability /// Copy with method for immutability
ProjectSubmission copyWith({ ProjectSubmission copyWith({
String? submissionId, String? submissionId,
String? userId, String? designedArea,
String? projectName, String? addressOfProject,
String? projectAddress, String? projectOwner,
double? projectValue, String? designFirm,
ProjectType? projectType, String? constructionContractor,
List<String>? beforePhotos, double? designArea,
List<String>? afterPhotos, String? productsIncludedInTheDesign,
List<String>? invoices, String? projectProgress,
SubmissionStatus? status, DateTime? expectedCommencementDate,
String? reviewNotes, String? description,
String? rejectionReason, DateTime? requestDate,
int? pointsEarned, String? workflowState,
DateTime? submittedAt, String? reasonForRejection,
DateTime? reviewedAt, String? status,
String? reviewedBy, String? statusColor,
bool? isAllowModify,
bool? isAllowCancel,
List<ProjectFile>? filesList,
}) { }) {
return ProjectSubmission( return ProjectSubmission(
submissionId: submissionId ?? this.submissionId, submissionId: submissionId ?? this.submissionId,
userId: userId ?? this.userId, designedArea: designedArea ?? this.designedArea,
projectName: projectName ?? this.projectName, addressOfProject: addressOfProject ?? this.addressOfProject,
projectAddress: projectAddress ?? this.projectAddress, projectOwner: projectOwner ?? this.projectOwner,
projectValue: projectValue ?? this.projectValue, designFirm: designFirm ?? this.designFirm,
projectType: projectType ?? this.projectType, constructionContractor: constructionContractor ?? this.constructionContractor,
beforePhotos: beforePhotos ?? this.beforePhotos, designArea: designArea ?? this.designArea,
afterPhotos: afterPhotos ?? this.afterPhotos, productsIncludedInTheDesign: productsIncludedInTheDesign ?? this.productsIncludedInTheDesign,
invoices: invoices ?? this.invoices, projectProgress: projectProgress ?? this.projectProgress,
expectedCommencementDate: expectedCommencementDate ?? this.expectedCommencementDate,
description: description ?? this.description,
requestDate: requestDate ?? this.requestDate,
workflowState: workflowState ?? this.workflowState,
reasonForRejection: reasonForRejection ?? this.reasonForRejection,
status: status ?? this.status, status: status ?? this.status,
reviewNotes: reviewNotes ?? this.reviewNotes, statusColor: statusColor ?? this.statusColor,
rejectionReason: rejectionReason ?? this.rejectionReason, isAllowModify: isAllowModify ?? this.isAllowModify,
pointsEarned: pointsEarned ?? this.pointsEarned, isAllowCancel: isAllowCancel ?? this.isAllowCancel,
submittedAt: submittedAt ?? this.submittedAt, filesList: filesList ?? this.filesList,
reviewedAt: reviewedAt ?? this.reviewedAt,
reviewedBy: reviewedBy ?? this.reviewedBy,
); );
} }
@override @override
bool operator ==(Object other) { List<Object?> get props => [
if (identical(this, other)) return true; submissionId,
designedArea,
return other is ProjectSubmission && addressOfProject,
other.submissionId == submissionId && projectOwner,
other.userId == userId && designFirm,
other.projectName == projectName && constructionContractor,
other.projectValue == projectValue && designArea,
other.status == status; productsIncludedInTheDesign,
} projectProgress,
expectedCommencementDate,
@override description,
int get hashCode { requestDate,
return Object.hash(submissionId, userId, projectName, projectValue, status); workflowState,
} reasonForRejection,
status,
statusColor,
isAllowModify,
isAllowCancel,
filesList,
];
@override @override
String toString() { String toString() {
return 'ProjectSubmission(submissionId: $submissionId, projectName: $projectName, ' return 'ProjectSubmission(submissionId: $submissionId, designedArea: $designedArea, '
'projectValue: $projectValue, projectType: $projectType, status: $status, ' 'designArea: $designArea, status: $status, statusColor: $statusColor)';
'pointsEarned: $pointsEarned)';
} }
} }

View File

@@ -0,0 +1,38 @@
/// Project Type Enum
///
/// Represents the type of construction project.
library;
/// Project type enum
enum ProjectType {
/// Residential project
residential,
/// Commercial project
commercial,
/// Industrial project
industrial,
/// Public infrastructure
infrastructure,
/// Other type
other;
/// Get display name for project type
String get displayName {
switch (this) {
case ProjectType.residential:
return 'Residential';
case ProjectType.commercial:
return 'Commercial';
case ProjectType.industrial:
return 'Industrial';
case ProjectType.infrastructure:
return 'Infrastructure';
case ProjectType.other:
return 'Other';
}
}
}

View File

@@ -3,24 +3,55 @@
/// 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_submission.dart'; import 'package:worker/features/projects/domain/entities/project_submission.dart';
/// Submissions Repository /// Submissions Repository
/// ///
/// Defines contract for project submissions data operations. /// Defines contract for project submissions data operations.
abstract class SubmissionsRepository { abstract class SubmissionsRepository {
/// Get list of available project statuses
///
/// Uses cache-first pattern:
/// - Returns cached data if available
/// - Fetches from API and updates cache
/// - [forceRefresh] bypasses cache and fetches fresh data
Future<List<ProjectStatus>> getProjectStatusList({
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 limitPageLength = 0,
});
/// Get a single submission by ID /// Get project detail by name
Future<ProjectSubmission> getSubmissionById(String submissionId); /// Returns the full project detail as entity for form prefilling
Future<ProjectSubmission> getSubmissionDetail(String name);
/// Create a new project submission /// Save (create/update) a project submission
Future<ProjectSubmission> createSubmission(ProjectSubmission submission); /// Returns the project name (ID) from the API response
Future<String> saveSubmission(ProjectSubmissionRequest request);
/// Update an existing submission /// Upload a file for a project submission
Future<ProjectSubmission> updateSubmission(ProjectSubmission submission); /// [projectName] is the project ID returned from saveSubmission
/// [filePath] is the local path to the file
/// Delete a submission /// Returns the uploaded file URL
Future<void> deleteSubmission(String submissionId); Future<String> uploadProjectFile({
required String projectName,
required String filePath,
});
} }

View File

@@ -1,23 +0,0 @@
/// Get Submissions Use Case
///
/// Retrieves all project submissions for the current user.
library;
import 'package:worker/features/projects/domain/entities/project_submission.dart';
import 'package:worker/features/projects/domain/repositories/submissions_repository.dart';
/// Get Submissions Use Case
///
/// Business logic for retrieving project submissions.
class GetSubmissions {
const GetSubmissions(this._repository);
final SubmissionsRepository _repository;
/// Execute the use case
///
/// Returns list of all project submissions for the current user.
Future<List<ProjectSubmission>> call() async {
return await _repository.getSubmissions();
}
}

View File

@@ -5,16 +5,26 @@ library;
import 'dart:io'; import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
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/domain/entities/project_submission.dart';
import 'package:worker/features/projects/presentation/providers/submissions_provider.dart';
/// Project Submission Create Page /// Project Submission Create/Edit Page
class SubmissionCreatePage extends ConsumerStatefulWidget { class SubmissionCreatePage extends ConsumerStatefulWidget {
const SubmissionCreatePage({super.key}); const SubmissionCreatePage({super.key, this.submission});
/// Optional submission for editing mode
/// If null, creates new submission
/// If provided, prefills form and updates existing submission
final ProjectSubmission? submission;
@override @override
ConsumerState<SubmissionCreatePage> createState() => ConsumerState<SubmissionCreatePage> createState() =>
@@ -35,10 +45,75 @@ 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 = []; // New files to upload
bool _showStartDateField = false; List<ProjectFile> _existingFiles = []; // Existing files from API
bool _isSubmitting = false;
bool _isLoadingDetail = false;
/// Whether we're editing an existing submission
bool get isEditing => widget.submission != null;
@override
void initState() {
super.initState();
// Fetch full detail when editing
if (isEditing) {
_loadDetail();
}
}
/// Load full project detail from API for editing
Future<void> _loadDetail() async {
if (!isEditing) return;
setState(() => _isLoadingDetail = true);
try {
final detail = await ref.read(
submissionDetailProvider(widget.submission!.submissionId).future,
);
if (!mounted) return;
// Prefill form fields from entity
_projectNameController.text = detail.designedArea;
_addressController.text = detail.addressOfProject ?? '';
_ownerController.text = detail.projectOwner ?? '';
_designUnitController.text = detail.designFirm ?? '';
_constructionUnitController.text = detail.constructionContractor ?? '';
_areaController.text = detail.designArea.toString();
_productsController.text = detail.productsIncludedInTheDesign ?? '';
_descriptionController.text = detail.description ?? '';
// Set expected commencement date
_expectedStartDate = detail.expectedCommencementDate;
// Find matching progress from the list
final progressId = detail.projectProgress;
if (progressId != null) {
final progressList = await ref.read(projectProgressListProvider.future);
_selectedProgress = progressList.where((p) => p.id == progressId).firstOrNull;
}
// Set existing files from API
_existingFiles = detail.filesList;
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Lỗi tải thông tin: $e'),
backgroundColor: AppColors.danger,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoadingDetail = false);
}
}
}
@override @override
void dispose() { void dispose() {
@@ -66,9 +141,9 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
), ),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
title: const Text( title: Text(
'Đăng ký Công trình', isEditing ? 'Chỉnh sửa Dự án' : 'Đăng ký Công trình',
style: TextStyle(color: Colors.black), style: const TextStyle(color: Colors.black),
), ),
actions: [ actions: [
IconButton( IconButton(
@@ -85,7 +160,21 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
backgroundColor: AppColors.white, backgroundColor: AppColors.white,
centerTitle: false, centerTitle: false,
), ),
body: Form( body: _isLoadingDetail
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text(
'Đang tải thông tin dự án...',
style: TextStyle(color: AppColors.grey500),
),
],
),
)
: Form(
key: _formKey, key: _formKey,
child: ListView( child: ListView(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
@@ -217,10 +306,8 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
_buildProgressDropdown(), _buildProgressDropdown(),
if (_showStartDateField) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
_buildDateField(), _buildExpectedDateField(),
],
], ],
), ),
), ),
@@ -321,8 +408,41 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
), ),
), ),
// Existing files from API
if (_existingFiles.isNotEmpty) ...[
const SizedBox(height: 16),
const Text(
'Ảnh đã tải lên',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey500,
),
),
const SizedBox(height: 8),
..._existingFiles.asMap().entries.map((entry) {
final index = entry.key;
final file = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildExistingFileItem(file, index),
);
}),
],
// New files to upload
if (_uploadedFiles.isNotEmpty) ...[ if (_uploadedFiles.isNotEmpty) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
if (_existingFiles.isNotEmpty)
const Text(
'Ảnh mới',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey500,
),
),
if (_existingFiles.isNotEmpty) const SizedBox(height: 8),
..._uploadedFiles.asMap().entries.map((entry) { ..._uploadedFiles.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;
final file = entry.value; final file = entry.value;
@@ -434,6 +554,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 +579,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 +599,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 +679,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 +692,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 +718,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 +763,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 +818,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,
@@ -643,21 +856,114 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
); );
} }
Widget _buildExistingFileItem(ProjectFile file, int index) {
final fileName = file.fileUrl.split('/').last;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
border: Border.all(color: AppColors.success),
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
// Network image
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: file.fileUrl,
width: 48,
height: 48,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 48,
height: 48,
color: AppColors.grey100,
child: const Center(
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
errorWidget: (context, url, error) => Container(
width: 48,
height: 48,
color: AppColors.grey100,
child: const Center(
child: FaIcon(
FontAwesomeIcons.image,
size: 24,
color: AppColors.grey500,
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fileName,
style: const TextStyle(
fontWeight: FontWeight.w500,
color: AppColors.grey900,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
const Text(
'Đã tải lên',
style: TextStyle(
fontSize: 12,
color: AppColors.success,
),
),
],
),
),
// Checkmark icon
const FaIcon(
FontAwesomeIcons.circleCheck,
size: 16,
color: AppColors.success,
),
],
),
);
}
Widget _buildSubmitButton() { Widget _buildSubmitButton() {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
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 +981,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,12 +1031,28 @@ 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(
title: const Text('Xác nhận'), title: const Text('Xác nhận'),
content: const Text('Xác nhận gửi đăng ký công trình?'), content: Text(
isEditing
? 'Xác nhận cập nhật thông tin dự án?'
: 'Xác nhận gửi đăng ký công trình?',
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () => Navigator.pop(context, false),
@@ -744,15 +1066,79 @@ 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
// Include name field when editing (for update)
final request = ProjectSubmissionRequest(
name: isEditing ? widget.submission!.submissionId : null,
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( SnackBar(
content: Text( content: Text(
'Đăng ký công trình đã được gửi thành công!\nChúng tôi sẽ xem xét và liên hệ với bạn sớm nhất.', isEditing
? 'Cập nhật dự án thành công!'
: 'Đăng ký công trình đã được gửi thành công!\nChúng tôi sẽ xem xét và liên hệ với bạn sớm nhất.',
), ),
backgroundColor: AppColors.success,
), ),
); );
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();
} }
} }
} }

View File

@@ -21,6 +21,7 @@ class SubmissionsPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final submissionsAsync = ref.watch(filteredSubmissionsProvider); final submissionsAsync = ref.watch(filteredSubmissionsProvider);
final statusListAsync = ref.watch(projectStatusListProvider);
final filter = ref.watch(submissionsFilterProvider); final filter = ref.watch(submissionsFilterProvider);
final selectedStatus = filter.selectedStatus; final selectedStatus = filter.selectedStatus;
@@ -38,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),
], ],
@@ -53,7 +60,7 @@ class SubmissionsPage extends ConsumerWidget {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Mã dự án hoặc tên dự án', hintText: 'Mã dự án hoặc tên công trình',
prefixIcon: const Icon(Icons.search, color: AppColors.grey500), prefixIcon: const Icon(Icons.search, color: AppColors.grey500),
filled: true, filled: true,
fillColor: AppColors.white, fillColor: AppColors.white,
@@ -86,16 +93,23 @@ class SubmissionsPage extends ConsumerWidget {
onTap: () => ref.read(submissionsFilterProvider.notifier).clearStatusFilter(), onTap: () => ref.read(submissionsFilterProvider.notifier).clearStatusFilter(),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
...SubmissionStatus.values.map((status) => Padding( // Use projectStatusListProvider to get status options
statusListAsync.when(
data: (statuses) => Row(
children: statuses.map((status) => Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: _buildFilterChip( child: _buildFilterChip(
context, context,
ref, ref,
label: status.displayName, label: status.label,
isSelected: selectedStatus == status, isSelected: selectedStatus == status.label,
onTap: () => ref.read(submissionsFilterProvider.notifier).selectStatus(status), onTap: () => ref.read(submissionsFilterProvider.notifier).selectStatus(status.label),
),
)).toList(),
),
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
), ),
)),
], ],
), ),
), ),
@@ -157,7 +171,7 @@ class SubmissionsPage extends ConsumerWidget {
itemCount: submissions.length, itemCount: submissions.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final submission = submissions[index]; final submission = submissions[index];
return _buildSubmissionCard(context, submission); return _buildSubmissionCard(context, ref, submission);
}, },
), ),
); );
@@ -246,17 +260,22 @@ class SubmissionsPage extends ConsumerWidget {
); );
} }
Widget _buildSubmissionCard(BuildContext context, ProjectSubmission submission) { Widget _buildSubmissionCard(BuildContext context, WidgetRef ref, ProjectSubmission submission) {
return Card( return Card(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
elevation: 1, elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: InkWell( child: InkWell(
onTap: () { onTap: () async {
// TODO: Navigate to submission detail // Navigate to edit submission page
ScaffoldMessenger.of(context).showSnackBar( final result = await context.push<bool>(
SnackBar(content: Text('Chi tiết dự án ${submission.submissionId}')), RouteNames.submissionCreate,
extra: submission,
); );
if (result == true) {
// Refresh submissions list after successful update
ref.invalidate(allSubmissionsProvider);
}
}, },
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Container( child: Container(
@@ -268,19 +287,27 @@ class SubmissionsPage extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'#${submission.submissionId}', submission.designedArea,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.grey900, color: AppColors.grey900,
), ),
), ),
_buildStatusBadge(submission.status), _buildStatusBadge(submission.status, submission.statusColor),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Text(
// 'Tên công trình: ${submission.designedArea}',
// style: const TextStyle(
// fontSize: 14,
// color: AppColors.grey900,
// ),
// ),
// const SizedBox(height: 4),
Text( Text(
'Tên công trình: ${submission.projectName}', 'Ngày nộp: ${DateFormat('dd/MM/yyyy HH:mm').format(submission.requestDate)}',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: AppColors.grey900, color: AppColors.grey900,
@@ -288,21 +315,13 @@ class SubmissionsPage extends ConsumerWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Ngày nộp: ${DateFormat('dd/MM/yyyy').format(submission.submittedAt)}', 'Diện tích: ${submission.designArea}',
style: const TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
color: AppColors.grey500, color: AppColors.grey900,
), ),
), ),
const SizedBox(height: 4), if (submission.reasonForRejection != null) ...[
Text(
'Diện tích: ${submission.projectAddress ?? "N/A"}',
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
),
if (submission.rejectionReason != null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
@@ -320,7 +339,7 @@ class SubmissionsPage extends ConsumerWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
submission.rejectionReason!, submission.reasonForRejection!,
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: AppColors.danger, color: AppColors.danger,
@@ -338,8 +357,8 @@ class SubmissionsPage extends ConsumerWidget {
); );
} }
Widget _buildStatusBadge(SubmissionStatus status) { Widget _buildStatusBadge(String status, String statusColor) {
final color = _getStatusColor(status); final color = _getColorFromStatusColor(statusColor);
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -347,7 +366,7 @@ class SubmissionsPage extends ConsumerWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
status.displayName, status,
style: TextStyle( style: TextStyle(
color: color, color: color,
fontSize: 12, fontSize: 12,
@@ -357,16 +376,18 @@ class SubmissionsPage extends ConsumerWidget {
); );
} }
Color _getStatusColor(SubmissionStatus status) { Color _getColorFromStatusColor(String statusColor) {
switch (status) { switch (statusColor) {
case SubmissionStatus.pending: case 'Warning':
return AppColors.warning; return AppColors.warning;
case SubmissionStatus.reviewing: case 'Success':
return AppColors.info;
case SubmissionStatus.approved:
return AppColors.success; return AppColors.success;
case SubmissionStatus.rejected: case 'Danger':
return AppColors.danger; return AppColors.danger;
case 'Info':
return AppColors.info;
default:
return AppColors.grey500;
} }
} }
} }

View File

@@ -4,51 +4,122 @@
library; 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/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/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_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';
import 'package:worker/features/projects/domain/usecases/get_submissions.dart';
part 'submissions_provider.g.dart'; part 'submissions_provider.g.dart';
/// Project Status Local Data Source Provider
@riverpod
ProjectStatusLocalDataSource projectStatusLocalDataSource(Ref ref) {
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
SubmissionsRemoteDataSource submissionsRemoteDataSource(Ref ref) { Future<SubmissionsRemoteDataSource> submissionsRemoteDataSource(Ref ref) async {
return SubmissionsRemoteDataSourceImpl(); final dioClient = await ref.watch(dioClientProvider.future);
return SubmissionsRemoteDataSourceImpl(dioClient);
} }
/// Submissions Repository Provider /// Submissions Repository Provider
@riverpod @riverpod
SubmissionsRepository submissionsRepository(Ref ref) { Future<SubmissionsRepository> submissionsRepository(Ref ref) async {
final remoteDataSource = ref.watch(submissionsRemoteDataSourceProvider); final remoteDataSource = await ref.watch(submissionsRemoteDataSourceProvider.future);
return SubmissionsRepositoryImpl(remoteDataSource); final statusLocalDataSource = ref.watch(projectStatusLocalDataSourceProvider);
final progressLocalDataSource = ref.watch(projectProgressLocalDataSourceProvider);
return SubmissionsRepositoryImpl(
remoteDataSource,
statusLocalDataSource,
progressLocalDataSource,
);
} }
/// Get Submissions Use Case Provider /// Project Status List Provider
///
/// Fetches project status options from API with cache-first pattern.
/// This is loaded before submissions to ensure filter options are available.
@riverpod @riverpod
GetSubmissions getSubmissions(Ref ref) { class ProjectStatusList extends _$ProjectStatusList {
final repository = ref.watch(submissionsRepositoryProvider); @override
return GetSubmissions(repository); Future<List<ProjectStatus>> build() async {
final repository = await ref.watch(submissionsRepositoryProvider.future);
return repository.getProjectStatusList();
}
/// Refresh status 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.getProjectStatusList(forceRefresh: true);
});
}
}
/// 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 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 {
final useCase = ref.watch(getSubmissionsProvider); // Ensure status list and progress list are loaded first (for filter options)
return await useCase(); await ref.watch(projectStatusListProvider.future);
await ref.watch(projectProgressListProvider.future);
// Then fetch submissions
final repository = await ref.watch(submissionsRepositoryProvider.future);
return repository.getSubmissions();
} }
/// Refresh submissions from remote /// Refresh submissions from remote
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 {
final useCase = ref.read(getSubmissionsProvider); // Also refresh status list and progress list
return await useCase(); await ref.read(projectStatusListProvider.notifier).refresh();
await ref.read(projectProgressListProvider.notifier).refresh();
final repository = await ref.read(submissionsRepositoryProvider.future);
return repository.getSubmissions();
}); });
} }
} }
@@ -56,10 +127,11 @@ class AllSubmissions extends _$AllSubmissions {
/// Submissions Filter State /// Submissions Filter State
/// ///
/// Manages search and status filter state. /// Manages search and status filter state.
/// Status filter uses the status label string from API (e.g., "Chờ phê duyệt").
@riverpod @riverpod
class SubmissionsFilter extends _$SubmissionsFilter { class SubmissionsFilter extends _$SubmissionsFilter {
@override @override
({String searchQuery, SubmissionStatus? selectedStatus}) build() { ({String searchQuery, String? selectedStatus}) build() {
return (searchQuery: '', selectedStatus: null); return (searchQuery: '', selectedStatus: null);
} }
@@ -68,8 +140,8 @@ class SubmissionsFilter extends _$SubmissionsFilter {
state = (searchQuery: query, selectedStatus: state.selectedStatus); state = (searchQuery: query, selectedStatus: state.selectedStatus);
} }
/// Select a status filter /// Select a status filter (uses Vietnamese label from API)
void selectStatus(SubmissionStatus? status) { void selectStatus(String? status) {
state = (searchQuery: state.searchQuery, selectedStatus: status); state = (searchQuery: state.searchQuery, selectedStatus: status);
} }
@@ -100,7 +172,7 @@ AsyncValue<List<ProjectSubmission>> filteredSubmissions(Ref ref) {
return dataAsync.whenData((submissions) { return dataAsync.whenData((submissions) {
var filtered = submissions; var filtered = submissions;
// Filter by status // Filter by status (matches Vietnamese label from API)
if (filter.selectedStatus != null) { if (filter.selectedStatus != null) {
filtered = filtered.where((s) => s.status == filter.selectedStatus).toList(); filtered = filtered.where((s) => s.status == filter.selectedStatus).toList();
} }
@@ -110,13 +182,172 @@ AsyncValue<List<ProjectSubmission>> filteredSubmissions(Ref ref) {
final query = filter.searchQuery.toLowerCase(); final query = filter.searchQuery.toLowerCase();
filtered = filtered.where((s) { filtered = filtered.where((s) {
return s.submissionId.toLowerCase().contains(query) || return s.submissionId.toLowerCase().contains(query) ||
s.projectName.toLowerCase().contains(query); s.designedArea.toLowerCase().contains(query);
}).toList(); }).toList();
} }
// Sort by submitted date (newest first) // Sort by request date (newest first)
filtered.sort((a, b) => b.submittedAt.compareTo(a.submittedAt)); filtered.sort((a, b) => b.requestDate.compareTo(a.requestDate));
return filtered; return filtered;
}); });
} }
/// Submission Detail Provider
///
/// Fetches full project detail by name for editing.
/// Uses family modifier to cache by submission name.
@riverpod
Future<ProjectSubmission> submissionDetail(Ref ref, String name) async {
final repository = await ref.watch(submissionsRepositoryProvider.future);
return repository.getSubmissionDetail(name);
}
/// Save Submission Provider
///
/// 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 = [];
}
}

View File

@@ -8,6 +8,116 @@ part of 'submissions_provider.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning // ignore_for_file: type=lint, type=warning
/// Project Status Local Data Source Provider
@ProviderFor(projectStatusLocalDataSource)
const projectStatusLocalDataSourceProvider =
ProjectStatusLocalDataSourceProvider._();
/// Project Status Local Data Source Provider
final class ProjectStatusLocalDataSourceProvider
extends
$FunctionalProvider<
ProjectStatusLocalDataSource,
ProjectStatusLocalDataSource,
ProjectStatusLocalDataSource
>
with $Provider<ProjectStatusLocalDataSource> {
/// Project Status Local Data Source Provider
const ProjectStatusLocalDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'projectStatusLocalDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$projectStatusLocalDataSourceHash();
@$internal
@override
$ProviderElement<ProjectStatusLocalDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ProjectStatusLocalDataSource create(Ref ref) {
return projectStatusLocalDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProjectStatusLocalDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProjectStatusLocalDataSource>(value),
);
}
}
String _$projectStatusLocalDataSourceHash() =>
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)
@@ -19,11 +129,13 @@ const submissionsRemoteDataSourceProvider =
final class SubmissionsRemoteDataSourceProvider final class SubmissionsRemoteDataSourceProvider
extends extends
$FunctionalProvider< $FunctionalProvider<
AsyncValue<SubmissionsRemoteDataSource>,
SubmissionsRemoteDataSource, SubmissionsRemoteDataSource,
SubmissionsRemoteDataSource, FutureOr<SubmissionsRemoteDataSource>
SubmissionsRemoteDataSource
> >
with $Provider<SubmissionsRemoteDataSource> { with
$FutureModifier<SubmissionsRemoteDataSource>,
$FutureProvider<SubmissionsRemoteDataSource> {
/// Submissions Remote Data Source Provider /// Submissions Remote Data Source Provider
const SubmissionsRemoteDataSourceProvider._() const SubmissionsRemoteDataSourceProvider._()
: super( : super(
@@ -41,26 +153,18 @@ final class SubmissionsRemoteDataSourceProvider
@$internal @$internal
@override @override
$ProviderElement<SubmissionsRemoteDataSource> $createElement( $FutureProviderElement<SubmissionsRemoteDataSource> $createElement(
$ProviderPointer pointer, $ProviderPointer pointer,
) => $ProviderElement(pointer); ) => $FutureProviderElement(pointer);
@override @override
SubmissionsRemoteDataSource create(Ref ref) { FutureOr<SubmissionsRemoteDataSource> create(Ref ref) {
return submissionsRemoteDataSource(ref); return submissionsRemoteDataSource(ref);
} }
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SubmissionsRemoteDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SubmissionsRemoteDataSource>(value),
);
}
} }
String _$submissionsRemoteDataSourceHash() => String _$submissionsRemoteDataSourceHash() =>
r'dc2dd71b6ca22d26382c1dfdf13b88d2249bb5ce'; r'ffaa92dd55ef50c8f1166773a83cd5c8cc16ded4';
/// Submissions Repository Provider /// Submissions Repository Provider
@@ -72,11 +176,13 @@ const submissionsRepositoryProvider = SubmissionsRepositoryProvider._();
final class SubmissionsRepositoryProvider final class SubmissionsRepositoryProvider
extends extends
$FunctionalProvider< $FunctionalProvider<
AsyncValue<SubmissionsRepository>,
SubmissionsRepository, SubmissionsRepository,
SubmissionsRepository, FutureOr<SubmissionsRepository>
SubmissionsRepository
> >
with $Provider<SubmissionsRepository> { with
$FutureModifier<SubmissionsRepository>,
$FutureProvider<SubmissionsRepository> {
/// Submissions Repository Provider /// Submissions Repository Provider
const SubmissionsRepositoryProvider._() const SubmissionsRepositoryProvider._()
: super( : super(
@@ -94,76 +200,157 @@ final class SubmissionsRepositoryProvider
@$internal @$internal
@override @override
$ProviderElement<SubmissionsRepository> $createElement( $FutureProviderElement<SubmissionsRepository> $createElement(
$ProviderPointer pointer, $ProviderPointer pointer,
) => $ProviderElement(pointer); ) => $FutureProviderElement(pointer);
@override @override
SubmissionsRepository create(Ref ref) { FutureOr<SubmissionsRepository> create(Ref ref) {
return submissionsRepository(ref); return submissionsRepository(ref);
} }
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SubmissionsRepository value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SubmissionsRepository>(value),
);
}
} }
String _$submissionsRepositoryHash() => String _$submissionsRepositoryHash() =>
r'4fa33107966470c07f050b27e669ec1dc4f13fda'; r'652208a4ef93cde9b40ae66164d44bba786dfed0';
/// Get Submissions Use Case Provider /// Project Status List Provider
///
/// Fetches project status options from API with cache-first pattern.
/// This is loaded before submissions to ensure filter options are available.
@ProviderFor(getSubmissions) @ProviderFor(ProjectStatusList)
const getSubmissionsProvider = GetSubmissionsProvider._(); const projectStatusListProvider = ProjectStatusListProvider._();
/// Get Submissions Use Case Provider /// Project Status List Provider
///
final class GetSubmissionsProvider /// Fetches project status options from API with cache-first pattern.
extends $FunctionalProvider<GetSubmissions, GetSubmissions, GetSubmissions> /// This is loaded before submissions to ensure filter options are available.
with $Provider<GetSubmissions> { final class ProjectStatusListProvider
/// Get Submissions Use Case Provider extends $AsyncNotifierProvider<ProjectStatusList, List<ProjectStatus>> {
const GetSubmissionsProvider._() /// Project Status List Provider
///
/// Fetches project status options from API with cache-first pattern.
/// This is loaded before submissions to ensure filter options are available.
const ProjectStatusListProvider._()
: super( : super(
from: null, from: null,
argument: null, argument: null,
retry: null, retry: null,
name: r'getSubmissionsProvider', name: r'projectStatusListProvider',
isAutoDispose: true, isAutoDispose: true,
dependencies: null, dependencies: null,
$allTransitiveDependencies: null, $allTransitiveDependencies: null,
); );
@override @override
String debugGetCreateSourceHash() => _$getSubmissionsHash(); String debugGetCreateSourceHash() => _$projectStatusListHash();
@$internal @$internal
@override @override
$ProviderElement<GetSubmissions> $createElement($ProviderPointer pointer) => ProjectStatusList create() => ProjectStatusList();
$ProviderElement(pointer); }
String _$projectStatusListHash() => r'69a43b619738dec3a6643a9a780599417403b838';
/// Project Status List Provider
///
/// Fetches project status options from API with cache-first pattern.
/// This is loaded before submissions to ensure filter options are available.
abstract class _$ProjectStatusList extends $AsyncNotifier<List<ProjectStatus>> {
FutureOr<List<ProjectStatus>> build();
@$mustCallSuper
@override @override
GetSubmissions create(Ref ref) { void runBuild() {
return getSubmissions(ref); final created = build();
} final ref =
this.ref as $Ref<AsyncValue<List<ProjectStatus>>, List<ProjectStatus>>;
/// {@macro riverpod.override_with_value} final element =
Override overrideWithValue(GetSubmissions value) { ref.element
return $ProviderOverride( as $ClassProviderElement<
origin: this, AnyNotifier<AsyncValue<List<ProjectStatus>>, List<ProjectStatus>>,
providerOverride: $SyncValueProvider<GetSubmissions>(value), AsyncValue<List<ProjectStatus>>,
); Object?,
Object?
>;
element.handleValue(ref, created);
} }
} }
String _$getSubmissionsHash() => r'91b497f826ae6dc72618ba879289fc449a7ef5cb'; /// 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 and progress list to be loaded first.
@ProviderFor(AllSubmissions) @ProviderFor(AllSubmissions)
const allSubmissionsProvider = AllSubmissionsProvider._(); const allSubmissionsProvider = AllSubmissionsProvider._();
@@ -171,11 +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 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 and progress list to be loaded first.
const AllSubmissionsProvider._() const AllSubmissionsProvider._()
: super( : super(
from: null, from: null,
@@ -195,11 +384,12 @@ final class AllSubmissionsProvider
AllSubmissions create() => AllSubmissions(); AllSubmissions create() => AllSubmissions();
} }
String _$allSubmissionsHash() => r'40ea0460a8962a4105dabb482bc80573452d4c80'; 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 and progress list to be loaded first.
abstract class _$AllSubmissions abstract class _$AllSubmissions
extends $AsyncNotifier<List<ProjectSubmission>> { extends $AsyncNotifier<List<ProjectSubmission>> {
@@ -232,6 +422,7 @@ abstract class _$AllSubmissions
/// Submissions Filter State /// Submissions Filter State
/// ///
/// Manages search and status filter state. /// Manages search and status filter state.
/// Status filter uses the status label string from API (e.g., "Chờ phê duyệt").
@ProviderFor(SubmissionsFilter) @ProviderFor(SubmissionsFilter)
const submissionsFilterProvider = SubmissionsFilterProvider._(); const submissionsFilterProvider = SubmissionsFilterProvider._();
@@ -239,15 +430,17 @@ const submissionsFilterProvider = SubmissionsFilterProvider._();
/// Submissions Filter State /// Submissions Filter State
/// ///
/// Manages search and status filter state. /// Manages search and status filter state.
/// Status filter uses the status label string from API (e.g., "Chờ phê duyệt").
final class SubmissionsFilterProvider final class SubmissionsFilterProvider
extends extends
$NotifierProvider< $NotifierProvider<
SubmissionsFilter, SubmissionsFilter,
({String searchQuery, SubmissionStatus? selectedStatus}) ({String searchQuery, String? selectedStatus})
> { > {
/// Submissions Filter State /// Submissions Filter State
/// ///
/// Manages search and status filter state. /// Manages search and status filter state.
/// Status filter uses the status label string from API (e.g., "Chờ phê duyệt").
const SubmissionsFilterProvider._() const SubmissionsFilterProvider._()
: super( : super(
from: null, from: null,
@@ -268,28 +461,28 @@ final class SubmissionsFilterProvider
/// {@macro riverpod.override_with_value} /// {@macro riverpod.override_with_value}
Override overrideWithValue( Override overrideWithValue(
({String searchQuery, SubmissionStatus? selectedStatus}) value, ({String searchQuery, String? selectedStatus}) value,
) { ) {
return $ProviderOverride( return $ProviderOverride(
origin: this, origin: this,
providerOverride: providerOverride:
$SyncValueProvider< $SyncValueProvider<({String searchQuery, String? selectedStatus})>(
({String searchQuery, SubmissionStatus? selectedStatus}) value,
>(value), ),
); );
} }
} }
String _$submissionsFilterHash() => r'049dd9fa4f6f1bff0d49c6cba0975f9714621883'; String _$submissionsFilterHash() => r'b3c59003922b1786b71f68726f97b210eed94c89';
/// Submissions Filter State /// Submissions Filter State
/// ///
/// Manages search and status filter state. /// Manages search and status filter state.
/// Status filter uses the status label string from API (e.g., "Chờ phê duyệt").
abstract class _$SubmissionsFilter abstract class _$SubmissionsFilter
extends extends $Notifier<({String searchQuery, String? selectedStatus})> {
$Notifier<({String searchQuery, SubmissionStatus? selectedStatus})> { ({String searchQuery, String? selectedStatus}) build();
({String searchQuery, SubmissionStatus? selectedStatus}) build();
@$mustCallSuper @$mustCallSuper
@override @override
void runBuild() { void runBuild() {
@@ -297,17 +490,17 @@ abstract class _$SubmissionsFilter
final ref = final ref =
this.ref this.ref
as $Ref< as $Ref<
({String searchQuery, SubmissionStatus? selectedStatus}), ({String searchQuery, String? selectedStatus}),
({String searchQuery, SubmissionStatus? selectedStatus}) ({String searchQuery, String? selectedStatus})
>; >;
final element = final element =
ref.element ref.element
as $ClassProviderElement< as $ClassProviderElement<
AnyNotifier< AnyNotifier<
({String searchQuery, SubmissionStatus? selectedStatus}), ({String searchQuery, String? selectedStatus}),
({String searchQuery, SubmissionStatus? selectedStatus}) ({String searchQuery, String? selectedStatus})
>, >,
({String searchQuery, SubmissionStatus? selectedStatus}), ({String searchQuery, String? selectedStatus}),
Object?, Object?,
Object? Object?
>; >;
@@ -374,4 +567,244 @@ final class FilteredSubmissionsProvider
} }
String _$filteredSubmissionsHash() => String _$filteredSubmissionsHash() =>
r'd0a07ab78a0d98596f01d0ed0a25016d573db5aa'; r'5be22b3242426c6b0c2f9778eaee5c7cf23e4814';
/// Submission Detail Provider
///
/// Fetches full project detail by name for editing.
/// Uses family modifier to cache by submission name.
@ProviderFor(submissionDetail)
const submissionDetailProvider = SubmissionDetailFamily._();
/// Submission Detail Provider
///
/// Fetches full project detail by name for editing.
/// Uses family modifier to cache by submission name.
final class SubmissionDetailProvider
extends
$FunctionalProvider<
AsyncValue<ProjectSubmission>,
ProjectSubmission,
FutureOr<ProjectSubmission>
>
with
$FutureModifier<ProjectSubmission>,
$FutureProvider<ProjectSubmission> {
/// Submission Detail Provider
///
/// Fetches full project detail by name for editing.
/// Uses family modifier to cache by submission name.
const SubmissionDetailProvider._({
required SubmissionDetailFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'submissionDetailProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$submissionDetailHash();
@override
String toString() {
return r'submissionDetailProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<ProjectSubmission> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<ProjectSubmission> create(Ref ref) {
final argument = this.argument as String;
return submissionDetail(ref, argument);
}
@override
bool operator ==(Object other) {
return other is SubmissionDetailProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$submissionDetailHash() => r'd3c767aa55e74a36c6a2b9b9bf6dd8ad8bf8eff3';
/// Submission Detail Provider
///
/// Fetches full project detail by name for editing.
/// Uses family modifier to cache by submission name.
final class SubmissionDetailFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<ProjectSubmission>, String> {
const SubmissionDetailFamily._()
: super(
retry: null,
name: r'submissionDetailProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Submission Detail Provider
///
/// Fetches full project detail by name for editing.
/// Uses family modifier to cache by submission name.
SubmissionDetailProvider call(String name) =>
SubmissionDetailProvider._(argument: name, from: this);
@override
String toString() => r'submissionDetailProvider';
}
/// Save Submission Provider
///
/// 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);
}
}

View File

@@ -32,7 +32,8 @@ 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_submission_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/quotes/data/models/quote_item_model.dart'; import 'package:worker/features/quotes/data/models/quote_item_model.dart';
import 'package:worker/features/quotes/data/models/quote_model.dart'; import 'package:worker/features/quotes/data/models/quote_model.dart';
import 'package:worker/features/showrooms/data/models/showroom_model.dart'; import 'package:worker/features/showrooms/data/models/showroom_model.dart';
@@ -76,7 +77,8 @@ extension HiveRegistrar on HiveInterface {
registerAdapter(PointsRecordModelAdapter()); registerAdapter(PointsRecordModelAdapter());
registerAdapter(PointsStatusAdapter()); registerAdapter(PointsStatusAdapter());
registerAdapter(ProductModelAdapter()); registerAdapter(ProductModelAdapter());
registerAdapter(ProjectSubmissionModelAdapter()); registerAdapter(ProjectProgressModelAdapter());
registerAdapter(ProjectStatusModelAdapter());
registerAdapter(ProjectTypeAdapter()); registerAdapter(ProjectTypeAdapter());
registerAdapter(PromotionModelAdapter()); registerAdapter(PromotionModelAdapter());
registerAdapter(QuoteItemModelAdapter()); registerAdapter(QuoteItemModelAdapter());
@@ -135,7 +137,8 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
registerAdapter(PointsRecordModelAdapter()); registerAdapter(PointsRecordModelAdapter());
registerAdapter(PointsStatusAdapter()); registerAdapter(PointsStatusAdapter());
registerAdapter(ProductModelAdapter()); registerAdapter(ProductModelAdapter());
registerAdapter(ProjectSubmissionModelAdapter()); registerAdapter(ProjectProgressModelAdapter());
registerAdapter(ProjectStatusModelAdapter());
registerAdapter(ProjectTypeAdapter()); registerAdapter(ProjectTypeAdapter());
registerAdapter(PromotionModelAdapter()); registerAdapter(PromotionModelAdapter());
registerAdapter(QuoteItemModelAdapter()); registerAdapter(QuoteItemModelAdapter());

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.1+16 version: 1.0.1+18
environment: environment:
sdk: ^3.10.0 sdk: ^3.10.0