This commit is contained in:
Phuoc Nguyen
2025-11-27 14:59:48 +07:00
parent dc8e60f589
commit ba04576750
25 changed files with 931 additions and 721 deletions

64
docs/projects.sh Normal file
View File

@@ -0,0 +1,64 @@
#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"
}
]
}

View File

@@ -272,31 +272,45 @@ 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';
/// 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,9 @@ 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';
/// 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 +80,7 @@ class HiveBoxNames {
cityBox, cityBox,
wardBox, wardBox,
orderStatusBox, orderStatusBox,
projectStatusBox,
settingsBox, settingsBox,
cacheBox, cacheBox,
syncStateBox, syncStateBox,
@@ -139,6 +143,7 @@ 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;
// Enums (33-61) // Enums (33-61)
static const int userRole = 33; static const int userRole = 33;
@@ -239,6 +244,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,15 @@ 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( debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.entryType) ? "" : ""} EntryType adapter', 'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.entryType) ? "" : ""} EntryType adapter',
); );
@@ -171,6 +177,9 @@ 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),
]); ]);
// Open potentially encrypted boxes (sensitive data) // Open potentially encrypted boxes (sensitive data)

View File

@@ -189,7 +189,7 @@ final class LoggingInterceptorProvider
} }
String _$loggingInterceptorHash() => String _$loggingInterceptorHash() =>
r'6afa480caa6fcd723dab769bb01601b8a37e20fd'; r'4d3147e9084d261e14653386ecd74ee471993af4';
/// Provider for ErrorTransformerInterceptor /// Provider for ErrorTransformerInterceptor

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,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,107 @@
/// 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:worker/core/constants/api_constants.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/features/projects/data/models/project_status_model.dart';
import 'package:worker/features/projects/data/models/project_submission_model.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 all submissions from remote API /// Fetch all submissions from remote API
Future<List<ProjectSubmission>> getSubmissions(); Future<List<ProjectSubmissionModel>> getSubmissions({
int limitStart = 0,
/// Fetch a single submission by ID int limitPageLength = 0,
Future<ProjectSubmission> getSubmissionById(String submissionId); });
/// Create a new submission
Future<ProjectSubmission> createSubmission(ProjectSubmission submission);
/// Update an existing submission
Future<ProjectSubmission> updateSubmission(ProjectSubmission submission);
/// Delete a submission
Future<void> deleteSubmission(String submissionId);
} }
/// 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');
} }
// API returns: { "message": [...] }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getProjectStatusList response');
}
final List<dynamic> statusList = message as List<dynamic>;
return statusList
.map((json) =>
ProjectStatusModel.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
throw Exception('Failed to get project status 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 @override
Future<ProjectSubmission> createSubmission( Future<List<ProjectSubmissionModel>> getSubmissions({
ProjectSubmission submission, int limitStart = 0,
) async { int limitPageLength = 0,
// Simulate network delay }) async {
await Future<void>.delayed(const Duration(milliseconds: 800)); try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getProjectList}',
data: {
'limit_start': limitStart,
'limit_page_length': limitPageLength,
},
);
// In real implementation, this would call the API final data = response.data;
return submission; if (data == null) {
throw Exception('No data received from getProjectList API');
} }
@override // API returns: { "message": [...] }
Future<ProjectSubmission> updateSubmission( final message = data['message'];
ProjectSubmission submission, if (message == null) {
) async { throw Exception('No message field in getProjectList response');
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 600));
// In real implementation, this would call the API
return submission;
} }
@override final List<dynamic> submissionsList = message as List<dynamic>;
Future<void> deleteSubmission(String submissionId) async { return submissionsList
// Simulate network delay .map((json) =>
await Future<void>.delayed(const Duration(milliseconds: 400)); ProjectSubmissionModel.fromJson(json as Map<String, dynamic>))
.toList();
// In real implementation, this would call the API } catch (e) {
throw Exception('Failed to get project submissions: $e');
}
} }
} }

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,105 @@
import 'dart:convert'; /// Project Submission Model
///
/// Data model for project submission from API responses with Hive caching.
/// Based on API response from building_material.building_material.api.project.get_list
library;
import 'package:hive_ce/hive.dart'; import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart'; import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart'; import 'package:worker/features/projects/domain/entities/project_submission.dart';
part 'project_submission_model.g.dart'; part 'project_submission_model.g.dart';
/// Project Submission Model - Type ID: 14
@HiveType(typeId: HiveTypeIds.projectSubmissionModel) @HiveType(typeId: HiveTypeIds.projectSubmissionModel)
class ProjectSubmissionModel extends HiveObject { class ProjectSubmissionModel extends HiveObject {
ProjectSubmissionModel({ /// Unique submission identifier (API: name)
required this.submissionId,
required this.userId,
required this.projectName,
required this.projectAddress,
required this.projectValue,
required this.projectType,
this.beforePhotos,
this.afterPhotos,
this.invoices,
required this.status,
this.reviewNotes,
this.rejectionReason,
this.pointsEarned,
required this.submittedAt,
this.reviewedAt,
this.reviewedBy,
});
@HiveField(0) @HiveField(0)
final String submissionId; final String submissionId;
/// Project name/title (API: designed_area)
@HiveField(1) @HiveField(1)
final String userId; final String designedArea;
/// Design area value in square meters (API: design_area)
@HiveField(2) @HiveField(2)
final String projectName; final double designArea;
/// Submission/request date (API: request_date)
@HiveField(3) @HiveField(3)
final String projectAddress; final DateTime requestDate;
/// Status label - Vietnamese (API: status)
@HiveField(4) @HiveField(4)
final double projectValue; final String status;
/// Rejection reason if rejected (API: reason_for_rejection)
@HiveField(5) @HiveField(5)
final ProjectType projectType; final String? reasonForRejection;
/// Status color indicator (API: status_color)
@HiveField(6) @HiveField(6)
final String? beforePhotos; final String statusColor;
@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( ProjectSubmissionModel({
Map<String, dynamic> json, required this.submissionId,
) => ProjectSubmissionModel( required this.designedArea,
submissionId: json['submission_id'] as String, required this.designArea,
userId: json['user_id'] as String, required this.requestDate,
projectName: json['project_name'] as String, required this.status,
projectAddress: json['project_address'] as String, this.reasonForRejection,
projectValue: (json['project_value'] as num).toDouble(), required this.statusColor,
projectType: ProjectType.values.firstWhere( });
(e) => e.name == json['project_type'],
), /// Create from JSON (API response)
beforePhotos: json['before_photos'] != null factory ProjectSubmissionModel.fromJson(Map<String, dynamic> json) {
? jsonEncode(json['before_photos']) return ProjectSubmissionModel(
: null, submissionId: json['name'] as String,
afterPhotos: json['after_photos'] != null designedArea: json['designed_area'] as String,
? jsonEncode(json['after_photos']) designArea: (json['design_area'] as num).toDouble(),
: null, requestDate: DateTime.parse(json['request_date'] as String),
invoices: json['invoices'] != null ? jsonEncode(json['invoices']) : null, status: json['status'] as String,
status: SubmissionStatus.values.firstWhere((e) => e.name == json['status']), reasonForRejection: json['reason_for_rejection'] as String?,
reviewNotes: json['review_notes'] as String?, statusColor: json['status_color'] 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': submissionId,
'project_address': projectAddress, 'designed_area': designedArea,
'project_value': projectValue, 'design_area': designArea,
'project_type': projectType.name, 'request_date': requestDate.toIso8601String(),
'before_photos': beforePhotos != null ? jsonDecode(beforePhotos!) : null, 'status': status,
'after_photos': afterPhotos != null ? jsonDecode(afterPhotos!) : null, 'reason_for_rejection': reasonForRejection,
'invoices': invoices != null ? jsonDecode(invoices!) : null, 'status_color': statusColor,
'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; ProjectSubmission toEntity() {
try { return ProjectSubmission(
final decoded = jsonDecode(afterPhotos!) as List; submissionId: submissionId,
return decoded.map((e) => e.toString()).toList(); designedArea: designedArea,
} catch (e) { designArea: designArea,
return null; requestDate: requestDate,
} status: status,
reasonForRejection: reasonForRejection,
statusColor: statusColor,
);
}
/// 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,
);
} }
} }

View File

@@ -19,60 +19,33 @@ class ProjectSubmissionModelAdapter
}; };
return ProjectSubmissionModel( return ProjectSubmissionModel(
submissionId: fields[0] as String, submissionId: fields[0] as String,
userId: fields[1] as String, designedArea: fields[1] as String,
projectName: fields[2] as String, designArea: (fields[2] as num).toDouble(),
projectAddress: fields[3] as String, requestDate: fields[3] as DateTime,
projectValue: (fields[4] as num).toDouble(), status: fields[4] as String,
projectType: fields[5] as ProjectType, reasonForRejection: fields[5] as String?,
beforePhotos: fields[6] as String?, statusColor: 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 @override
void write(BinaryWriter writer, ProjectSubmissionModel obj) { void write(BinaryWriter writer, ProjectSubmissionModel obj) {
writer writer
..writeByte(16) ..writeByte(7)
..writeByte(0) ..writeByte(0)
..write(obj.submissionId) ..write(obj.submissionId)
..writeByte(1) ..writeByte(1)
..write(obj.userId) ..write(obj.designedArea)
..writeByte(2) ..writeByte(2)
..write(obj.projectName) ..write(obj.designArea)
..writeByte(3) ..writeByte(3)
..write(obj.projectAddress) ..write(obj.requestDate)
..writeByte(4) ..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) ..write(obj.status)
..writeByte(10) ..writeByte(5)
..write(obj.reviewNotes) ..write(obj.reasonForRejection)
..writeByte(11) ..writeByte(6)
..write(obj.rejectionReason) ..write(obj.statusColor);
..writeByte(12)
..write(obj.pointsEarned)
..writeByte(13)
..write(obj.submittedAt)
..writeByte(14)
..write(obj.reviewedAt)
..writeByte(15)
..write(obj.reviewedBy);
} }
@override @override

View File

@@ -1,66 +1,85 @@
/// 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_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/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,
);
const SubmissionsRepositoryImpl(this._remoteDataSource);
final SubmissionsRemoteDataSource _remoteDataSource; final SubmissionsRemoteDataSource _remoteDataSource;
final ProjectStatusLocalDataSource _statusLocalDataSource;
/// 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) { } catch (e) {
rethrow; // Silently fail - we already returned cached data
} }
} }
@override @override
Future<ProjectSubmission> createSubmission( Future<List<ProjectSubmission>> getSubmissions({
ProjectSubmission submission, int limitStart = 0,
) async { int limitPageLength = 0,
}) async {
try { try {
return await _remoteDataSource.createSubmission(submission); final submissionModels = await _remoteDataSource.getSubmissions(
} catch (e) { limitStart: limitStart,
rethrow; limitPageLength: limitPageLength,
} );
} return submissionModels.map((model) => model.toEntity()).toList();
@override
Future<ProjectSubmission> updateSubmission(
ProjectSubmission submission,
) async {
try {
return await _remoteDataSource.updateSubmission(submission);
} catch (e) {
rethrow;
}
}
@override
Future<void> deleteSubmission(String submissionId) async {
try {
await _remoteDataSource.deleteSubmission(submissionId);
} 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,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,100 @@
/// 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_list
library; library;
/// Project type enum import 'package:equatable/equatable.dart';
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';
}
}
}
/// 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 /// - design_area -> designArea (area value in m²)
/// - Points earned /// - request_date -> requestDate
class ProjectSubmission { /// - status -> status (Vietnamese label)
/// Unique submission identifier /// - reason_for_rejection -> reasonForRejection
/// - status_color -> statusColor
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 /// Design area value in square meters (API: design_area)
final String projectName; final double designArea;
/// Project address/location /// Submission/request date (API: request_date)
final String? projectAddress; final DateTime requestDate;
/// Project value/cost /// Status label - Vietnamese (API: status)
final double projectValue; /// e.g., "Chờ phê duyệt", "Đã được phê duyệt", "Từ chối", "HỦY BỎ"
final String status;
/// Project type /// Rejection reason if rejected (API: reason_for_rejection)
final ProjectType projectType; final String? reasonForRejection;
/// Before photos URLs /// Status color indicator (API: status_color)
final List<String> beforePhotos; /// Values: "Warning", "Success", "Danger"
final String statusColor;
/// After photos URLs
final List<String> afterPhotos;
/// Invoice/receipt URLs
final List<String> invoices;
/// Submission status
final SubmissionStatus status;
/// Review notes from admin
final String? reviewNotes;
/// Rejection reason (if rejected)
final String? rejectionReason;
/// Points earned (if approved)
final int? pointsEarned;
/// Submission timestamp
final DateTime submittedAt;
/// Review timestamp
final DateTime? reviewedAt;
/// ID of admin who reviewed
final String? reviewedBy;
const ProjectSubmission({ const ProjectSubmission({
required this.submissionId, required this.submissionId,
required this.userId, required this.designedArea,
required this.projectName, required this.designArea,
this.projectAddress, required this.requestDate,
required this.projectValue,
required this.projectType,
required this.beforePhotos,
required this.afterPhotos,
required this.invoices,
required this.status, required this.status,
this.reviewNotes, this.reasonForRejection,
this.rejectionReason, required this.statusColor,
this.pointsEarned,
required this.submittedAt,
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, double? designArea,
String? projectAddress, DateTime? requestDate,
double? projectValue, String? status,
ProjectType? projectType, String? reasonForRejection,
List<String>? beforePhotos, String? statusColor,
List<String>? afterPhotos,
List<String>? invoices,
SubmissionStatus? status,
String? reviewNotes,
String? rejectionReason,
int? pointsEarned,
DateTime? submittedAt,
DateTime? reviewedAt,
String? reviewedBy,
}) { }) {
return ProjectSubmission( return ProjectSubmission(
submissionId: submissionId ?? this.submissionId, submissionId: submissionId ?? this.submissionId,
userId: userId ?? this.userId, designedArea: designedArea ?? this.designedArea,
projectName: projectName ?? this.projectName, designArea: designArea ?? this.designArea,
projectAddress: projectAddress ?? this.projectAddress, requestDate: requestDate ?? this.requestDate,
projectValue: projectValue ?? this.projectValue,
projectType: projectType ?? this.projectType,
beforePhotos: beforePhotos ?? this.beforePhotos,
afterPhotos: afterPhotos ?? this.afterPhotos,
invoices: invoices ?? this.invoices,
status: status ?? this.status, status: status ?? this.status,
reviewNotes: reviewNotes ?? this.reviewNotes, reasonForRejection: reasonForRejection ?? this.reasonForRejection,
rejectionReason: rejectionReason ?? this.rejectionReason, statusColor: statusColor ?? this.statusColor,
pointsEarned: pointsEarned ?? this.pointsEarned,
submittedAt: submittedAt ?? this.submittedAt,
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 && designArea,
other.submissionId == submissionId && requestDate,
other.userId == userId && status,
other.projectName == projectName && reasonForRejection,
other.projectValue == projectValue && statusColor,
other.status == status; ];
}
@override
int get hashCode {
return Object.hash(submissionId, userId, projectName, projectValue, status);
}
@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,26 @@
/// Repository interface for project submissions operations. /// Repository interface for project submissions operations.
library; library;
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 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,
/// Get a single submission by ID int limitPageLength = 0,
Future<ProjectSubmission> getSubmissionById(String submissionId); });
/// Create a new project submission
Future<ProjectSubmission> createSubmission(ProjectSubmission submission);
/// Update an existing submission
Future<ProjectSubmission> updateSubmission(ProjectSubmission submission);
/// Delete a submission
Future<void> deleteSubmission(String submissionId);
} }

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

@@ -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;
@@ -53,7 +54,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 +87,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(),
), ),
)),
], ],
), ),
), ),
@@ -268,19 +276,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 +304,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 +328,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 +346,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 +355,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 +365,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,84 @@
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_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/repositories/submissions_repository_impl.dart'; import 'package:worker/features/projects/data/repositories/submissions_repository_impl.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();
}
/// 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);
return SubmissionsRepositoryImpl(remoteDataSource, statusLocalDataSource);
} }
/// 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);
});
}
} }
/// All Submissions Provider /// All Submissions Provider
/// ///
/// Fetches and manages submissions data from remote. /// Fetches and manages submissions data from remote.
/// Waits for project status list to be loaded first.
@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 is loaded first (for filter options)
return await useCase(); await ref.watch(projectStatusListProvider.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
return await useCase(); await ref.read(projectStatusListProvider.notifier).refresh();
final repository = await ref.read(submissionsRepositoryProvider.future);
return repository.getSubmissions();
}); });
} }
} }
@@ -56,10 +89,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 +102,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 +134,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,12 +144,12 @@ 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;
}); });

View File

@@ -8,6 +8,60 @@ 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';
/// Submissions Remote Data Source Provider /// Submissions Remote Data Source Provider
@ProviderFor(submissionsRemoteDataSource) @ProviderFor(submissionsRemoteDataSource)
@@ -19,11 +73,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 +97,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 +120,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 +144,87 @@ 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'd8261cc538c1fdaa47064e4945302b80f49098bb';
/// 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';
/// All Submissions Provider /// All Submissions Provider
/// ///
/// Fetches and manages submissions data from remote. /// Fetches and manages submissions data from remote.
/// Waits for project status list to be loaded first.
@ProviderFor(AllSubmissions) @ProviderFor(AllSubmissions)
const allSubmissionsProvider = AllSubmissionsProvider._(); const allSubmissionsProvider = AllSubmissionsProvider._();
@@ -171,11 +232,13 @@ const allSubmissionsProvider = AllSubmissionsProvider._();
/// All Submissions Provider /// All Submissions Provider
/// ///
/// Fetches and manages submissions data from remote. /// Fetches and manages submissions data from remote.
/// Waits for project status list to be loaded first.
final class AllSubmissionsProvider final class AllSubmissionsProvider
extends $AsyncNotifierProvider<AllSubmissions, List<ProjectSubmission>> { extends $AsyncNotifierProvider<AllSubmissions, List<ProjectSubmission>> {
/// All Submissions Provider /// All Submissions Provider
/// ///
/// Fetches and manages submissions data from remote. /// Fetches and manages submissions data from remote.
/// Waits for project status list to be loaded first.
const AllSubmissionsProvider._() const AllSubmissionsProvider._()
: super( : super(
from: null, from: null,
@@ -195,11 +258,12 @@ final class AllSubmissionsProvider
AllSubmissions create() => AllSubmissions(); AllSubmissions create() => AllSubmissions();
} }
String _$allSubmissionsHash() => r'40ea0460a8962a4105dabb482bc80573452d4c80'; String _$allSubmissionsHash() => r'a4a7fb0d2953efb21e2e6343429f7550c763ea85';
/// All Submissions Provider /// All Submissions Provider
/// ///
/// Fetches and manages submissions data from remote. /// Fetches and manages submissions data from remote.
/// Waits for project status list to be loaded first.
abstract class _$AllSubmissions abstract class _$AllSubmissions
extends $AsyncNotifier<List<ProjectSubmission>> { extends $AsyncNotifier<List<ProjectSubmission>> {
@@ -232,6 +296,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 +304,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 +335,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 +364,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 +441,4 @@ final class FilteredSubmissionsProvider
} }
String _$filteredSubmissionsHash() => String _$filteredSubmissionsHash() =>
r'd0a07ab78a0d98596f01d0ed0a25016d573db5aa'; r'5be22b3242426c6b0c2f9778eaee5c7cf23e4814';

View File

@@ -32,6 +32,7 @@ import 'package:worker/features/products/data/models/category_model.dart';
import 'package:worker/features/products/data/models/product_model.dart'; import 'package:worker/features/products/data/models/product_model.dart';
import 'package:worker/features/products/data/models/stock_level_model.dart'; import 'package:worker/features/products/data/models/stock_level_model.dart';
import 'package:worker/features/projects/data/models/design_request_model.dart'; import 'package:worker/features/projects/data/models/design_request_model.dart';
import 'package:worker/features/projects/data/models/project_status_model.dart';
import 'package:worker/features/projects/data/models/project_submission_model.dart'; import 'package:worker/features/projects/data/models/project_submission_model.dart';
import 'package:worker/features/quotes/data/models/quote_item_model.dart'; import 'package:worker/features/quotes/data/models/quote_item_model.dart';
import 'package:worker/features/quotes/data/models/quote_model.dart'; import 'package:worker/features/quotes/data/models/quote_model.dart';
@@ -76,6 +77,7 @@ extension HiveRegistrar on HiveInterface {
registerAdapter(PointsRecordModelAdapter()); registerAdapter(PointsRecordModelAdapter());
registerAdapter(PointsStatusAdapter()); registerAdapter(PointsStatusAdapter());
registerAdapter(ProductModelAdapter()); registerAdapter(ProductModelAdapter());
registerAdapter(ProjectStatusModelAdapter());
registerAdapter(ProjectSubmissionModelAdapter()); registerAdapter(ProjectSubmissionModelAdapter());
registerAdapter(ProjectTypeAdapter()); registerAdapter(ProjectTypeAdapter());
registerAdapter(PromotionModelAdapter()); registerAdapter(PromotionModelAdapter());
@@ -135,6 +137,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
registerAdapter(PointsRecordModelAdapter()); registerAdapter(PointsRecordModelAdapter());
registerAdapter(PointsStatusAdapter()); registerAdapter(PointsStatusAdapter());
registerAdapter(ProductModelAdapter()); registerAdapter(ProductModelAdapter());
registerAdapter(ProjectStatusModelAdapter());
registerAdapter(ProjectSubmissionModelAdapter()); registerAdapter(ProjectSubmissionModelAdapter());
registerAdapter(ProjectTypeAdapter()); registerAdapter(ProjectTypeAdapter());
registerAdapter(PromotionModelAdapter()); registerAdapter(PromotionModelAdapter());

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