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

View File

@@ -272,31 +272,45 @@ class ApiConstants {
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
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}
static const String getProjects = '/projects';
/// Get project details by ID
/// Get project details by ID (legacy endpoint - may be deprecated)
/// GET /projects/{projectId}
static const String getProjectDetails = '/projects';
/// Update project
/// Update project (legacy endpoint - may be deprecated)
/// PUT /projects/{projectId}
static const String updateProject = '/projects';
/// Update project progress
/// Update project progress (legacy endpoint - may be deprecated)
/// PATCH /projects/{projectId}/progress
/// Body: { "progress": 75 }
static const String updateProjectProgress = '/projects';
/// Delete project
/// Delete project (legacy endpoint - may be deprecated)
/// DELETE /projects/{projectId}
static const String deleteProject = '/projects';

View File

@@ -64,6 +64,9 @@ class HiveBoxNames {
/// Order status list cache
static const String orderStatusBox = 'order_status_box';
/// Project status list cache
static const String projectStatusBox = 'project_status_box';
/// Get all box names for initialization
static List<String> get allBoxes => [
userBox,
@@ -77,6 +80,7 @@ class HiveBoxNames {
cityBox,
wardBox,
orderStatusBox,
projectStatusBox,
settingsBox,
cacheBox,
syncStateBox,
@@ -139,6 +143,7 @@ class HiveTypeIds {
static const int cityModel = 31;
static const int wardModel = 32;
static const int orderStatusModel = 62;
static const int projectStatusModel = 63;
// Enums (33-61)
static const int userRole = 33;
@@ -239,6 +244,37 @@ class OrderStatusIndex {
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)
extension HiveKeysContinued on HiveKeys {
// Cache Box Keys

View File

@@ -102,9 +102,15 @@ class HiveService {
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatus) ? "" : ""} OrderStatus adapter',
);
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatusModel) ? "" : ""} OrderStatusModel adapter',
);
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectType) ? "" : ""} ProjectType adapter',
);
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectStatusModel) ? "" : ""} ProjectStatusModel adapter',
);
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.entryType) ? "" : ""} EntryType adapter',
);
@@ -171,6 +177,9 @@ class HiveService {
// Order status box (non-sensitive) - caches order status list from API
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)

View File

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

View File

@@ -272,7 +272,7 @@ final class AuthProvider extends $AsyncNotifierProvider<Auth, User?> {
Auth create() => Auth();
}
String _$authHash() => r'f1a16022d628a21f230c0bb567e80ff6e293d840';
String _$authHash() => r'f0438cf6eb9eb17c0afc6b23055acd09926b21ae';
/// 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.
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
///
/// Abstract interface for remote submissions operations.
/// Interface for remote project submission operations.
abstract class SubmissionsRemoteDataSource {
/// Fetch project status list from API
Future<List<ProjectStatusModel>> getProjectStatusList();
/// Fetch all submissions from remote API
Future<List<ProjectSubmission>> getSubmissions();
/// Fetch a single submission by ID
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);
Future<List<ProjectSubmissionModel>> getSubmissions({
int limitStart = 0,
int limitPageLength = 0,
});
}
/// 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 {
@override
Future<List<ProjectSubmission>> getSubmissions() async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 500));
const SubmissionsRemoteDataSourceImpl(this._dioClient);
return [
ProjectSubmission(
submissionId: 'DA001',
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,
),
];
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
Future<List<ProjectStatusModel>> getProjectStatusList() async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getProjectStatusList}',
data: <String, dynamic>{
'limit_start': 0,
'limit_page_length': 0,
},
);
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
Future<ProjectSubmission> getSubmissionById(String submissionId) async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 300));
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 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 getProjectList API');
}
@override
Future<ProjectSubmission> createSubmission(
ProjectSubmission submission,
) async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 800));
// API returns: { "message": [...] }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getProjectList response');
}
// In real implementation, this would call the API
return submission;
}
@override
Future<ProjectSubmission> updateSubmission(
ProjectSubmission submission,
) async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 600));
// In real implementation, this would call the API
return submission;
}
@override
Future<void> deleteSubmission(String submissionId) async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 400));
// In real implementation, this would call the API
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');
}
}
}

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: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';
/// Project Submission Model - Type ID: 14
@HiveType(typeId: HiveTypeIds.projectSubmissionModel)
class ProjectSubmissionModel extends HiveObject {
ProjectSubmissionModel({
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,
});
/// Unique submission identifier (API: name)
@HiveField(0)
final String submissionId;
/// Project name/title (API: designed_area)
@HiveField(1)
final String userId;
final String designedArea;
/// Design area value in square meters (API: design_area)
@HiveField(2)
final String projectName;
final double designArea;
/// Submission/request date (API: request_date)
@HiveField(3)
final String projectAddress;
final DateTime requestDate;
/// Status label - Vietnamese (API: status)
@HiveField(4)
final double projectValue;
final String status;
/// Rejection reason if rejected (API: reason_for_rejection)
@HiveField(5)
final ProjectType projectType;
final String? reasonForRejection;
/// Status color indicator (API: status_color)
@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;
final String statusColor;
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?,
);
ProjectSubmissionModel({
required this.submissionId,
required this.designedArea,
required this.designArea,
required this.requestDate,
required this.status,
this.reasonForRejection,
required this.statusColor,
});
Map<String, dynamic> toJson() => {
'submission_id': submissionId,
'user_id': userId,
'project_name': projectName,
'project_address': projectAddress,
'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;
}
/// Create from JSON (API response)
factory ProjectSubmissionModel.fromJson(Map<String, dynamic> json) {
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,
);
}
List<String>? get afterPhotosList {
if (afterPhotos == null) return null;
try {
final decoded = jsonDecode(afterPhotos!) as List;
return decoded.map((e) => e.toString()).toList();
} catch (e) {
return null;
}
/// 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,
};
}
/// Convert to entity
ProjectSubmission toEntity() {
return ProjectSubmission(
submissionId: submissionId,
designedArea: designedArea,
designArea: designArea,
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(
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?,
designedArea: fields[1] as String,
designArea: (fields[2] as num).toDouble(),
requestDate: fields[3] as DateTime,
status: fields[4] as String,
reasonForRejection: fields[5] as String?,
statusColor: fields[6] as String,
);
}
@override
void write(BinaryWriter writer, ProjectSubmissionModel obj) {
writer
..writeByte(16)
..writeByte(7)
..writeByte(0)
..write(obj.submissionId)
..writeByte(1)
..write(obj.userId)
..write(obj.designedArea)
..writeByte(2)
..write(obj.projectName)
..write(obj.designArea)
..writeByte(3)
..write(obj.projectAddress)
..write(obj.requestDate)
..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);
..writeByte(5)
..write(obj.reasonForRejection)
..writeByte(6)
..write(obj.statusColor);
}
@override

View File

@@ -1,66 +1,85 @@
/// Submissions Repository Implementation
///
/// Implements the submissions repository interface.
/// Implements the submissions repository interface with caching support.
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/domain/entities/project_status.dart';
import 'package:worker/features/projects/domain/entities/project_submission.dart';
import 'package:worker/features/projects/domain/repositories/submissions_repository.dart';
/// Submissions Repository Implementation
///
/// Handles data operations for project submissions.
/// Handles data operations for project submissions with cache-first pattern.
class SubmissionsRepositoryImpl implements SubmissionsRepository {
const SubmissionsRepositoryImpl(
this._remoteDataSource,
this._statusLocalDataSource,
);
const SubmissionsRepositoryImpl(this._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
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 {
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) {
// In real implementation, handle errors properly
// For now, rethrow
// If API fails, try to return cached data as fallback
final cachedStatuses = _statusLocalDataSource.getCachedStatusList();
if (cachedStatuses.isNotEmpty) {
return cachedStatuses.map((model) => model.toEntity()).toList();
}
rethrow;
}
}
@override
Future<ProjectSubmission> getSubmissionById(String submissionId) async {
/// Refresh status cache in background
Future<void> _refreshStatusCache() async {
try {
return await _remoteDataSource.getSubmissionById(submissionId);
final statusModels = await _remoteDataSource.getProjectStatusList();
await _statusLocalDataSource.cacheStatusList(statusModels);
} catch (e) {
rethrow;
// Silently fail - we already returned cached data
}
}
@override
Future<ProjectSubmission> createSubmission(
ProjectSubmission submission,
) async {
Future<List<ProjectSubmission>> getSubmissions({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
return await _remoteDataSource.createSubmission(submission);
} catch (e) {
rethrow;
}
}
@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);
final submissionModels = await _remoteDataSource.getSubmissions(
limitStart: limitStart,
limitPageLength: limitPageLength,
);
return submissionModels.map((model) => model.toEntity()).toList();
} catch (e) {
rethrow;
}

View File

@@ -3,7 +3,7 @@
/// Represents a request for design consultation service.
library;
import 'project_submission.dart';
import 'package:worker/features/projects/domain/entities/project_type.dart';
/// Design status enum
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
///
/// Represents a completed project submitted for loyalty points.
/// Based on API response from building_material.building_material.api.project.get_list
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';
}
}
}
/// 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';
}
}
}
import 'package:equatable/equatable.dart';
/// Project Submission Entity
///
/// Contains information about a completed project:
/// - Project details
/// - Before/after photos
/// - Invoice documentation
/// - Review status
/// - Points earned
class ProjectSubmission {
/// Unique submission identifier
/// Contains information about a completed project submission.
/// Mapped from API response:
/// - name -> submissionId
/// - designed_area -> designedArea (project name/title)
/// - design_area -> designArea (area value in m²)
/// - request_date -> requestDate
/// - status -> status (Vietnamese label)
/// - reason_for_rejection -> reasonForRejection
/// - status_color -> statusColor
class ProjectSubmission extends Equatable {
/// Unique submission identifier (API: name)
final String submissionId;
/// User ID who submitted
final String userId;
/// Project name/title (API: designed_area)
final String designedArea;
/// Project name
final String projectName;
/// Design area value in square meters (API: design_area)
final double designArea;
/// Project address/location
final String? projectAddress;
/// Submission/request date (API: request_date)
final DateTime requestDate;
/// Project value/cost
final double projectValue;
/// Status label - Vietnamese (API: status)
/// e.g., "Chờ phê duyệt", "Đã được phê duyệt", "Từ chối", "HỦY BỎ"
final String status;
/// Project type
final ProjectType projectType;
/// Rejection reason if rejected (API: reason_for_rejection)
final String? reasonForRejection;
/// Before photos URLs
final List<String> beforePhotos;
/// 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;
/// Status color indicator (API: status_color)
/// Values: "Warning", "Success", "Danger"
final String statusColor;
const ProjectSubmission({
required this.submissionId,
required this.userId,
required this.projectName,
this.projectAddress,
required this.projectValue,
required this.projectType,
required this.beforePhotos,
required this.afterPhotos,
required this.invoices,
required this.designedArea,
required this.designArea,
required this.requestDate,
required this.status,
this.reviewNotes,
this.rejectionReason,
this.pointsEarned,
required this.submittedAt,
this.reviewedAt,
this.reviewedBy,
this.reasonForRejection,
required this.statusColor,
});
/// Check if submission is pending
bool get isPending => status == SubmissionStatus.pending;
/// Check if submission is under review
bool get isReviewing => status == SubmissionStatus.reviewing;
/// Check if submission is pending approval
bool get isPending => statusColor == 'Warning';
/// Check if submission is approved
bool get isApproved => status == SubmissionStatus.approved;
bool get isApproved => statusColor == 'Success';
/// Check if submission is rejected
bool get isRejected => status == SubmissionStatus.rejected;
/// 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);
}
/// Check if submission is rejected or cancelled
bool get isRejected => statusColor == 'Danger';
/// Copy with method for immutability
ProjectSubmission copyWith({
String? submissionId,
String? userId,
String? projectName,
String? projectAddress,
double? projectValue,
ProjectType? projectType,
List<String>? beforePhotos,
List<String>? afterPhotos,
List<String>? invoices,
SubmissionStatus? status,
String? reviewNotes,
String? rejectionReason,
int? pointsEarned,
DateTime? submittedAt,
DateTime? reviewedAt,
String? reviewedBy,
String? designedArea,
double? designArea,
DateTime? requestDate,
String? status,
String? reasonForRejection,
String? statusColor,
}) {
return ProjectSubmission(
submissionId: submissionId ?? this.submissionId,
userId: userId ?? this.userId,
projectName: projectName ?? this.projectName,
projectAddress: projectAddress ?? this.projectAddress,
projectValue: projectValue ?? this.projectValue,
projectType: projectType ?? this.projectType,
beforePhotos: beforePhotos ?? this.beforePhotos,
afterPhotos: afterPhotos ?? this.afterPhotos,
invoices: invoices ?? this.invoices,
designedArea: designedArea ?? this.designedArea,
designArea: designArea ?? this.designArea,
requestDate: requestDate ?? this.requestDate,
status: status ?? this.status,
reviewNotes: reviewNotes ?? this.reviewNotes,
rejectionReason: rejectionReason ?? this.rejectionReason,
pointsEarned: pointsEarned ?? this.pointsEarned,
submittedAt: submittedAt ?? this.submittedAt,
reviewedAt: reviewedAt ?? this.reviewedAt,
reviewedBy: reviewedBy ?? this.reviewedBy,
reasonForRejection: reasonForRejection ?? this.reasonForRejection,
statusColor: statusColor ?? this.statusColor,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ProjectSubmission &&
other.submissionId == submissionId &&
other.userId == userId &&
other.projectName == projectName &&
other.projectValue == projectValue &&
other.status == status;
}
@override
int get hashCode {
return Object.hash(submissionId, userId, projectName, projectValue, status);
}
List<Object?> get props => [
submissionId,
designedArea,
designArea,
requestDate,
status,
reasonForRejection,
statusColor,
];
@override
String toString() {
return 'ProjectSubmission(submissionId: $submissionId, projectName: $projectName, '
'projectValue: $projectValue, projectType: $projectType, status: $status, '
'pointsEarned: $pointsEarned)';
return 'ProjectSubmission(submissionId: $submissionId, designedArea: $designedArea, '
'designArea: $designArea, status: $status, statusColor: $statusColor)';
}
}

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.
library;
import 'package:worker/features/projects/domain/entities/project_status.dart';
import 'package:worker/features/projects/domain/entities/project_submission.dart';
/// Submissions Repository
///
/// Defines contract for project submissions data operations.
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
Future<List<ProjectSubmission>> getSubmissions();
/// Get a single submission by ID
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);
Future<List<ProjectSubmission>> getSubmissions({
int limitStart = 0,
int limitPageLength = 0,
});
}

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
Widget build(BuildContext context, WidgetRef ref) {
final submissionsAsync = ref.watch(filteredSubmissionsProvider);
final statusListAsync = ref.watch(projectStatusListProvider);
final filter = ref.watch(submissionsFilterProvider);
final selectedStatus = filter.selectedStatus;
@@ -53,7 +54,7 @@ class SubmissionsPage extends ConsumerWidget {
padding: const EdgeInsets.all(16),
child: TextField(
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),
filled: true,
fillColor: AppColors.white,
@@ -86,16 +87,23 @@ class SubmissionsPage extends ConsumerWidget {
onTap: () => ref.read(submissionsFilterProvider.notifier).clearStatusFilter(),
),
const SizedBox(width: 8),
...SubmissionStatus.values.map((status) => Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildFilterChip(
context,
ref,
label: status.displayName,
isSelected: selectedStatus == status,
onTap: () => ref.read(submissionsFilterProvider.notifier).selectStatus(status),
// Use projectStatusListProvider to get status options
statusListAsync.when(
data: (statuses) => Row(
children: statuses.map((status) => Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildFilterChip(
context,
ref,
label: status.label,
isSelected: selectedStatus == status.label,
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,
children: [
Text(
'#${submission.submissionId}',
submission.designedArea,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
_buildStatusBadge(submission.status),
_buildStatusBadge(submission.status, submission.statusColor),
],
),
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(
'Tên công trình: ${submission.projectName}',
'Ngày nộp: ${DateFormat('dd/MM/yyyy HH:mm').format(submission.requestDate)}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey900,
@@ -288,21 +304,13 @@ class SubmissionsPage extends ConsumerWidget {
),
const SizedBox(height: 4),
Text(
'Ngày nộp: ${DateFormat('dd/MM/yyyy').format(submission.submittedAt)}',
'Diện tích: ${submission.designArea}',
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
color: AppColors.grey900,
),
),
const SizedBox(height: 4),
Text(
'Diện tích: ${submission.projectAddress ?? "N/A"}',
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
),
if (submission.rejectionReason != null) ...[
if (submission.reasonForRejection != null) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
@@ -320,7 +328,7 @@ class SubmissionsPage extends ConsumerWidget {
const SizedBox(width: 8),
Expanded(
child: Text(
submission.rejectionReason!,
submission.reasonForRejection!,
style: const TextStyle(
fontSize: 12,
color: AppColors.danger,
@@ -338,8 +346,8 @@ class SubmissionsPage extends ConsumerWidget {
);
}
Widget _buildStatusBadge(SubmissionStatus status) {
final color = _getStatusColor(status);
Widget _buildStatusBadge(String status, String statusColor) {
final color = _getColorFromStatusColor(statusColor);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
@@ -347,7 +355,7 @@ class SubmissionsPage extends ConsumerWidget {
borderRadius: BorderRadius.circular(12),
),
child: Text(
status.displayName,
status,
style: TextStyle(
color: color,
fontSize: 12,
@@ -357,16 +365,18 @@ class SubmissionsPage extends ConsumerWidget {
);
}
Color _getStatusColor(SubmissionStatus status) {
switch (status) {
case SubmissionStatus.pending:
Color _getColorFromStatusColor(String statusColor) {
switch (statusColor) {
case 'Warning':
return AppColors.warning;
case SubmissionStatus.reviewing:
return AppColors.info;
case SubmissionStatus.approved:
case 'Success':
return AppColors.success;
case SubmissionStatus.rejected:
case 'Danger':
return AppColors.danger;
case 'Info':
return AppColors.info;
default:
return AppColors.grey500;
}
}
}

View File

@@ -4,51 +4,84 @@
library;
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/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/repositories/submissions_repository.dart';
import 'package:worker/features/projects/domain/usecases/get_submissions.dart';
part 'submissions_provider.g.dart';
/// Project Status Local Data Source Provider
@riverpod
ProjectStatusLocalDataSource projectStatusLocalDataSource(Ref ref) {
return ProjectStatusLocalDataSource();
}
/// Submissions Remote Data Source Provider
@riverpod
SubmissionsRemoteDataSource submissionsRemoteDataSource(Ref ref) {
return SubmissionsRemoteDataSourceImpl();
Future<SubmissionsRemoteDataSource> submissionsRemoteDataSource(Ref ref) async {
final dioClient = await ref.watch(dioClientProvider.future);
return SubmissionsRemoteDataSourceImpl(dioClient);
}
/// Submissions Repository Provider
@riverpod
SubmissionsRepository submissionsRepository(Ref ref) {
final remoteDataSource = ref.watch(submissionsRemoteDataSourceProvider);
return SubmissionsRepositoryImpl(remoteDataSource);
Future<SubmissionsRepository> submissionsRepository(Ref ref) async {
final remoteDataSource = await ref.watch(submissionsRemoteDataSourceProvider.future);
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
GetSubmissions getSubmissions(Ref ref) {
final repository = ref.watch(submissionsRepositoryProvider);
return GetSubmissions(repository);
class ProjectStatusList extends _$ProjectStatusList {
@override
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
///
/// Fetches and manages submissions data from remote.
/// Waits for project status list to be loaded first.
@riverpod
class AllSubmissions extends _$AllSubmissions {
@override
Future<List<ProjectSubmission>> build() async {
final useCase = ref.watch(getSubmissionsProvider);
return await useCase();
// Ensure status list is loaded first (for filter options)
await ref.watch(projectStatusListProvider.future);
// Then fetch submissions
final repository = await ref.watch(submissionsRepositoryProvider.future);
return repository.getSubmissions();
}
/// Refresh submissions from remote
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final useCase = ref.read(getSubmissionsProvider);
return await useCase();
// Also refresh status list
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
///
/// Manages search and status filter state.
/// Status filter uses the status label string from API (e.g., "Chờ phê duyệt").
@riverpod
class SubmissionsFilter extends _$SubmissionsFilter {
@override
({String searchQuery, SubmissionStatus? selectedStatus}) build() {
({String searchQuery, String? selectedStatus}) build() {
return (searchQuery: '', selectedStatus: null);
}
@@ -68,8 +102,8 @@ class SubmissionsFilter extends _$SubmissionsFilter {
state = (searchQuery: query, selectedStatus: state.selectedStatus);
}
/// Select a status filter
void selectStatus(SubmissionStatus? status) {
/// Select a status filter (uses Vietnamese label from API)
void selectStatus(String? status) {
state = (searchQuery: state.searchQuery, selectedStatus: status);
}
@@ -100,7 +134,7 @@ AsyncValue<List<ProjectSubmission>> filteredSubmissions(Ref ref) {
return dataAsync.whenData((submissions) {
var filtered = submissions;
// Filter by status
// Filter by status (matches Vietnamese label from API)
if (filter.selectedStatus != null) {
filtered = filtered.where((s) => s.status == filter.selectedStatus).toList();
}
@@ -110,12 +144,12 @@ AsyncValue<List<ProjectSubmission>> filteredSubmissions(Ref ref) {
final query = filter.searchQuery.toLowerCase();
filtered = filtered.where((s) {
return s.submissionId.toLowerCase().contains(query) ||
s.projectName.toLowerCase().contains(query);
s.designedArea.toLowerCase().contains(query);
}).toList();
}
// Sort by submitted date (newest first)
filtered.sort((a, b) => b.submittedAt.compareTo(a.submittedAt));
// Sort by request date (newest first)
filtered.sort((a, b) => b.requestDate.compareTo(a.requestDate));
return filtered;
});

View File

@@ -8,6 +8,60 @@ part of 'submissions_provider.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND
// 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
@ProviderFor(submissionsRemoteDataSource)
@@ -19,11 +73,13 @@ const submissionsRemoteDataSourceProvider =
final class SubmissionsRemoteDataSourceProvider
extends
$FunctionalProvider<
AsyncValue<SubmissionsRemoteDataSource>,
SubmissionsRemoteDataSource,
SubmissionsRemoteDataSource,
SubmissionsRemoteDataSource
FutureOr<SubmissionsRemoteDataSource>
>
with $Provider<SubmissionsRemoteDataSource> {
with
$FutureModifier<SubmissionsRemoteDataSource>,
$FutureProvider<SubmissionsRemoteDataSource> {
/// Submissions Remote Data Source Provider
const SubmissionsRemoteDataSourceProvider._()
: super(
@@ -41,26 +97,18 @@ final class SubmissionsRemoteDataSourceProvider
@$internal
@override
$ProviderElement<SubmissionsRemoteDataSource> $createElement(
$FutureProviderElement<SubmissionsRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
) => $FutureProviderElement(pointer);
@override
SubmissionsRemoteDataSource create(Ref ref) {
FutureOr<SubmissionsRemoteDataSource> create(Ref ref) {
return submissionsRemoteDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SubmissionsRemoteDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SubmissionsRemoteDataSource>(value),
);
}
}
String _$submissionsRemoteDataSourceHash() =>
r'dc2dd71b6ca22d26382c1dfdf13b88d2249bb5ce';
r'ffaa92dd55ef50c8f1166773a83cd5c8cc16ded4';
/// Submissions Repository Provider
@@ -72,11 +120,13 @@ const submissionsRepositoryProvider = SubmissionsRepositoryProvider._();
final class SubmissionsRepositoryProvider
extends
$FunctionalProvider<
AsyncValue<SubmissionsRepository>,
SubmissionsRepository,
SubmissionsRepository,
SubmissionsRepository
FutureOr<SubmissionsRepository>
>
with $Provider<SubmissionsRepository> {
with
$FutureModifier<SubmissionsRepository>,
$FutureProvider<SubmissionsRepository> {
/// Submissions Repository Provider
const SubmissionsRepositoryProvider._()
: super(
@@ -94,76 +144,87 @@ final class SubmissionsRepositoryProvider
@$internal
@override
$ProviderElement<SubmissionsRepository> $createElement(
$FutureProviderElement<SubmissionsRepository> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
) => $FutureProviderElement(pointer);
@override
SubmissionsRepository create(Ref ref) {
FutureOr<SubmissionsRepository> create(Ref ref) {
return submissionsRepository(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SubmissionsRepository value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SubmissionsRepository>(value),
);
}
}
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)
const getSubmissionsProvider = GetSubmissionsProvider._();
@ProviderFor(ProjectStatusList)
const projectStatusListProvider = ProjectStatusListProvider._();
/// Get Submissions Use Case Provider
final class GetSubmissionsProvider
extends $FunctionalProvider<GetSubmissions, GetSubmissions, GetSubmissions>
with $Provider<GetSubmissions> {
/// Get Submissions Use Case Provider
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.
final class ProjectStatusListProvider
extends $AsyncNotifierProvider<ProjectStatusList, List<ProjectStatus>> {
/// 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(
from: null,
argument: null,
retry: null,
name: r'getSubmissionsProvider',
name: r'projectStatusListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$getSubmissionsHash();
String debugGetCreateSourceHash() => _$projectStatusListHash();
@$internal
@override
$ProviderElement<GetSubmissions> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
GetSubmissions create(Ref ref) {
return getSubmissions(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GetSubmissions value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GetSubmissions>(value),
);
}
ProjectStatusList create() => ProjectStatusList();
}
String _$getSubmissionsHash() => r'91b497f826ae6dc72618ba879289fc449a7ef5cb';
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
void runBuild() {
final created = build();
final ref =
this.ref as $Ref<AsyncValue<List<ProjectStatus>>, List<ProjectStatus>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<ProjectStatus>>, List<ProjectStatus>>,
AsyncValue<List<ProjectStatus>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// All Submissions Provider
///
/// Fetches and manages submissions data from remote.
/// Waits for project status list to be loaded first.
@ProviderFor(AllSubmissions)
const allSubmissionsProvider = AllSubmissionsProvider._();
@@ -171,11 +232,13 @@ const allSubmissionsProvider = AllSubmissionsProvider._();
/// All Submissions Provider
///
/// Fetches and manages submissions data from remote.
/// Waits for project status list to be loaded first.
final class AllSubmissionsProvider
extends $AsyncNotifierProvider<AllSubmissions, List<ProjectSubmission>> {
/// All Submissions Provider
///
/// Fetches and manages submissions data from remote.
/// Waits for project status list to be loaded first.
const AllSubmissionsProvider._()
: super(
from: null,
@@ -195,11 +258,12 @@ final class AllSubmissionsProvider
AllSubmissions create() => AllSubmissions();
}
String _$allSubmissionsHash() => r'40ea0460a8962a4105dabb482bc80573452d4c80';
String _$allSubmissionsHash() => r'a4a7fb0d2953efb21e2e6343429f7550c763ea85';
/// All Submissions Provider
///
/// Fetches and manages submissions data from remote.
/// Waits for project status list to be loaded first.
abstract class _$AllSubmissions
extends $AsyncNotifier<List<ProjectSubmission>> {
@@ -232,6 +296,7 @@ abstract class _$AllSubmissions
/// Submissions 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)
const submissionsFilterProvider = SubmissionsFilterProvider._();
@@ -239,15 +304,17 @@ const submissionsFilterProvider = SubmissionsFilterProvider._();
/// Submissions 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
extends
$NotifierProvider<
SubmissionsFilter,
({String searchQuery, SubmissionStatus? selectedStatus})
({String searchQuery, String? selectedStatus})
> {
/// Submissions 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._()
: super(
from: null,
@@ -268,28 +335,28 @@ final class SubmissionsFilterProvider
/// {@macro riverpod.override_with_value}
Override overrideWithValue(
({String searchQuery, SubmissionStatus? selectedStatus}) value,
({String searchQuery, String? selectedStatus}) value,
) {
return $ProviderOverride(
origin: this,
providerOverride:
$SyncValueProvider<
({String searchQuery, SubmissionStatus? selectedStatus})
>(value),
$SyncValueProvider<({String searchQuery, String? selectedStatus})>(
value,
),
);
}
}
String _$submissionsFilterHash() => r'049dd9fa4f6f1bff0d49c6cba0975f9714621883';
String _$submissionsFilterHash() => r'b3c59003922b1786b71f68726f97b210eed94c89';
/// Submissions 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
extends
$Notifier<({String searchQuery, SubmissionStatus? selectedStatus})> {
({String searchQuery, SubmissionStatus? selectedStatus}) build();
extends $Notifier<({String searchQuery, String? selectedStatus})> {
({String searchQuery, String? selectedStatus}) build();
@$mustCallSuper
@override
void runBuild() {
@@ -297,17 +364,17 @@ abstract class _$SubmissionsFilter
final ref =
this.ref
as $Ref<
({String searchQuery, SubmissionStatus? selectedStatus}),
({String searchQuery, SubmissionStatus? selectedStatus})
({String searchQuery, String? selectedStatus}),
({String searchQuery, String? selectedStatus})
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
({String searchQuery, SubmissionStatus? selectedStatus}),
({String searchQuery, SubmissionStatus? selectedStatus})
({String searchQuery, String? selectedStatus}),
({String searchQuery, String? selectedStatus})
>,
({String searchQuery, SubmissionStatus? selectedStatus}),
({String searchQuery, String? selectedStatus}),
Object?,
Object?
>;
@@ -374,4 +441,4 @@ final class FilteredSubmissionsProvider
}
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/stock_level_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/quotes/data/models/quote_item_model.dart';
import 'package:worker/features/quotes/data/models/quote_model.dart';
@@ -76,6 +77,7 @@ extension HiveRegistrar on HiveInterface {
registerAdapter(PointsRecordModelAdapter());
registerAdapter(PointsStatusAdapter());
registerAdapter(ProductModelAdapter());
registerAdapter(ProjectStatusModelAdapter());
registerAdapter(ProjectSubmissionModelAdapter());
registerAdapter(ProjectTypeAdapter());
registerAdapter(PromotionModelAdapter());
@@ -135,6 +137,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
registerAdapter(PointsRecordModelAdapter());
registerAdapter(PointsStatusAdapter());
registerAdapter(ProductModelAdapter());
registerAdapter(ProjectStatusModelAdapter());
registerAdapter(ProjectSubmissionModelAdapter());
registerAdapter(ProjectTypeAdapter());
registerAdapter(PromotionModelAdapter());