request detail

This commit is contained in:
Phuoc Nguyen
2025-11-28 15:47:51 +07:00
parent 65f6f825a6
commit 9e7bda32f2
14 changed files with 1320 additions and 718 deletions

View File

@@ -316,13 +316,13 @@
</div> </div>
<div class="library-content" onclick="viewLibraryDetail('studio-apartment')"> <div class="library-content" onclick="viewLibraryDetail('studio-apartment')">
<h3 class="library-title">Căn hộ Studio</h3> <h3 class="library-title">Căn hộ Studio</h3>
<div class="library-date"> <!--<div class="library-date">
<i class="fas fa-calendar-alt"></i> <i class="fas fa-calendar-alt"></i>
<span>Ngày đăng: 15/11/2024</span> <span>Ngày đăng: 15/11/2024</span>
</div> </div>
<p class="library-description"> <p class="library-description">
Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa.
</p> </p>-->
</div> </div>
</div> </div>
@@ -336,13 +336,13 @@
</div> </div>
<div class="library-content"> <div class="library-content">
<h3 class="library-title">Biệt thự Hiện đại</h3> <h3 class="library-title">Biệt thự Hiện đại</h3>
<div class="library-date"> <!--<div class="library-date">
<i class="fas fa-calendar-alt"></i> <i class="fas fa-calendar-alt"></i>
<span>Ngày đăng: 12/11/2024</span> <span>Ngày đăng: 12/11/2024</span>
</div> </div>
<p class="library-description"> <p class="library-description">
Biệt thự 3 tầng với phong cách kiến trúc hiện đại, sử dụng gạch granite và ceramic premium tạo điểm nhấn. Biệt thự 3 tầng với phong cách kiến trúc hiện đại, sử dụng gạch granite và ceramic premium tạo điểm nhấn.
</p> </p>-->
</div> </div>
</div> </div>
@@ -356,13 +356,13 @@
</div> </div>
<div class="library-content"> <div class="library-content">
<h3 class="library-title">Nhà phố Tối giản</h3> <h3 class="library-title">Nhà phố Tối giản</h3>
<div class="library-date"> <!--<div class="library-date">
<i class="fas fa-calendar-alt"></i> <i class="fas fa-calendar-alt"></i>
<span>Ngày đăng: 08/11/2024</span> <span>Ngày đăng: 08/11/2024</span>
</div> </div>
<p class="library-description"> <p class="library-description">
Nhà phố 4x15m với thiết kế tối giản, tận dụng ánh sáng tự nhiên và gạch men màu trung tính. Nhà phố 4x15m với thiết kế tối giản, tận dụng ánh sáng tự nhiên và gạch men màu trung tính.
</p> </p>-->
</div> </div>
</div> </div>
@@ -376,13 +376,13 @@
</div> </div>
<div class="library-content"> <div class="library-content">
<h3 class="library-title">Chung cư Cao cấp</h3> <h3 class="library-title">Chung cư Cao cấp</h3>
<div class="library-date"> <!--<div class="library-date">
<i class="fas fa-calendar-alt"></i> <i class="fas fa-calendar-alt"></i>
<span>Ngày đăng: 05/11/2024</span> <span>Ngày đăng: 05/11/2024</span>
</div> </div>
<p class="library-description"> <p class="library-description">
Căn hộ 3PN với nội thất sang trọng, sử dụng gạch marble và ceramic cao cấp nhập khẩu Italy. Căn hộ 3PN với nội thất sang trọng, sử dụng gạch marble và ceramic cao cấp nhập khẩu Italy.
</p> </p>-->
</div> </div>
</div> </div>
</div> </div>

View File

@@ -338,6 +338,24 @@ class ApiConstants {
static const String getSampleProjectDetail = static const String getSampleProjectDetail =
'/building_material.building_material.api.sample_project.get_detail'; '/building_material.building_material.api.sample_project.get_detail';
// ============================================================================
// Design Request Endpoints (Frappe ERPNext)
// ============================================================================
/// Get list of design requests (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.design_request.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: { "message": [{ "name": "...", "subject": "...", "description": "...", "dateline": "...", "status": "...", "status_color": "..." }] }
static const String getDesignRequestList =
'/building_material.building_material.api.design_request.get_list';
/// Get detail of a design request (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.design_request.get_detail
/// Body: { "name": "ISS-2025-00005" }
/// Returns: { "message": { "name": "...", "subject": "...", "description": "...", "dateline": "...", "status": "...", "status_color": "...", "files_list": [...] } }
static const String getDesignRequestDetail =
'/building_material.building_material.api.design_request.get_detail';
/// Create new project (legacy endpoint - may be deprecated) /// Create new project (legacy endpoint - may be deprecated)
/// POST /projects /// POST /projects
static const String createProject = '/projects'; static const String createProject = '/projects';

View File

@@ -0,0 +1,96 @@
/// Design Request Remote Data Source
///
/// Handles remote API calls for design requests.
library;
import 'package:worker/core/constants/api_constants.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/features/showrooms/data/models/design_request_model.dart';
/// Design Request Remote Data Source Interface
abstract class DesignRequestRemoteDataSource {
/// Fetch list of design requests from API
Future<List<DesignRequestModel>> getDesignRequests({
int limitStart = 0,
int limitPageLength = 0,
});
/// Fetch detail of a design request by name
Future<DesignRequestModel> getDesignRequestDetail(String name);
}
/// Design Request Remote Data Source Implementation
class DesignRequestRemoteDataSourceImpl implements DesignRequestRemoteDataSource {
const DesignRequestRemoteDataSourceImpl(this._dioClient);
final DioClient _dioClient;
/// Get list of design requests
///
/// Calls: POST /api/method/building_material.building_material.api.design_request.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: List of design requests
@override
Future<List<DesignRequestModel>> getDesignRequests({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getDesignRequestList}',
data: {
'limit_start': limitStart,
'limit_page_length': limitPageLength,
},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getDesignRequestList API');
}
// API returns: { "message": [...] }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getDesignRequestList response');
}
final List<dynamic> requestsList = message as List<dynamic>;
return requestsList
.map((json) => DesignRequestModel.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
throw Exception('Failed to get design requests: $e');
}
}
/// Get detail of a design request by name
///
/// Calls: POST /api/method/building_material.building_material.api.design_request.get_detail
/// Body: { "name": "ISS-2025-00005" }
/// Returns: Full design request detail with files_list
@override
Future<DesignRequestModel> getDesignRequestDetail(String name) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getDesignRequestDetail}',
data: {'name': name},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getDesignRequestDetail API');
}
// API returns: { "message": {...} }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getDesignRequestDetail response');
}
return DesignRequestModel.fromJson(message as Map<String, dynamic>);
} catch (e) {
throw Exception('Failed to get design request detail: $e');
}
}
}

View File

@@ -0,0 +1,103 @@
/// Data Model: Design Request Model
///
/// JSON serialization model for design request API responses.
library;
import 'package:worker/features/showrooms/data/models/sample_project_model.dart';
import 'package:worker/features/showrooms/domain/entities/design_request.dart';
/// Design Request Model
///
/// Handles JSON serialization/deserialization for API communication.
class DesignRequestModel {
/// Unique request identifier (API: name)
final String name;
/// Request subject/title (API: subject)
final String subject;
/// Request description - may contain HTML (API: description)
final String? description;
/// Deadline date string (API: dateline)
final String? dateline;
/// Status display text (API: status)
final String status;
/// Status color code (API: status_color)
final String statusColor;
/// List of attached files (API: files_list) - available in detail
final List<ProjectFileModel> filesList;
const DesignRequestModel({
required this.name,
required this.subject,
this.description,
this.dateline,
required this.status,
required this.statusColor,
this.filesList = const [],
});
/// Create model from JSON map
factory DesignRequestModel.fromJson(Map<String, dynamic> json) {
final filesListJson = json['files_list'] as List<dynamic>?;
return DesignRequestModel(
name: json['name'] as String? ?? '',
subject: json['subject'] as String? ?? '',
description: json['description'] as String?,
dateline: json['dateline'] as String?,
status: json['status'] as String? ?? '',
statusColor: json['status_color'] as String? ?? '',
filesList: filesListJson != null
? filesListJson
.map((f) => ProjectFileModel.fromJson(f as Map<String, dynamic>))
.toList()
: [],
);
}
/// Convert model to JSON map
Map<String, dynamic> toJson() {
return {
'name': name,
'subject': subject,
'description': description,
'dateline': dateline,
'status': status,
'status_color': statusColor,
'files_list': filesList.map((f) => f.toJson()).toList(),
};
}
/// Convert to domain entity
DesignRequest toEntity() {
return DesignRequest(
id: name,
subject: subject,
description: description,
dateline: dateline,
statusText: status,
statusColor: statusColor,
filesList: filesList.map((f) => f.toEntity()).toList(),
);
}
/// Create model from domain entity
factory DesignRequestModel.fromEntity(DesignRequest entity) {
return DesignRequestModel(
name: entity.id,
subject: entity.subject,
description: entity.description,
dateline: entity.dateline,
status: entity.statusText,
statusColor: entity.statusColor,
filesList: entity.filesList
.map((f) => ProjectFileModel(name: f.id, fileUrl: f.fileUrl))
.toList(),
);
}
}

View File

@@ -5,24 +5,26 @@ library;
import 'package:worker/features/showrooms/domain/entities/sample_project.dart'; import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
/// Sample Project File Model /// Project File Model
/// ///
/// Handles JSON serialization for file attachments. /// Shared model for file attachments used by:
class SampleProjectFileModel { /// - SampleProjectModel (model houses)
/// - DesignRequestModel (design requests)
class ProjectFileModel {
/// Unique file identifier (API: name) /// Unique file identifier (API: name)
final String name; final String name;
/// Full URL to the file (API: file_url) /// Full URL to the file (API: file_url)
final String fileUrl; final String fileUrl;
const SampleProjectFileModel({ const ProjectFileModel({
required this.name, required this.name,
required this.fileUrl, required this.fileUrl,
}); });
/// Create model from JSON map /// Create model from JSON map
factory SampleProjectFileModel.fromJson(Map<String, dynamic> json) { factory ProjectFileModel.fromJson(Map<String, dynamic> json) {
return SampleProjectFileModel( return ProjectFileModel(
name: json['name'] as String? ?? '', name: json['name'] as String? ?? '',
fileUrl: json['file_url'] as String? ?? '', fileUrl: json['file_url'] as String? ?? '',
); );
@@ -37,8 +39,8 @@ class SampleProjectFileModel {
} }
/// Convert to domain entity /// Convert to domain entity
SampleProjectFile toEntity() { ProjectFile toEntity() {
return SampleProjectFile( return ProjectFile(
id: name, id: name,
fileUrl: fileUrl, fileUrl: fileUrl,
); );
@@ -65,7 +67,7 @@ class SampleProjectModel {
final String? thumbnail; final String? thumbnail;
/// List of attached files/images (API: files_list) - available in detail /// List of attached files/images (API: files_list) - available in detail
final List<SampleProjectFileModel> filesList; final List<ProjectFileModel> filesList;
const SampleProjectModel({ const SampleProjectModel({
required this.name, required this.name,
@@ -88,7 +90,7 @@ class SampleProjectModel {
thumbnail: json['thumbnail'] as String?, thumbnail: json['thumbnail'] as String?,
filesList: filesListJson != null filesList: filesListJson != null
? filesListJson ? filesListJson
.map((f) => SampleProjectFileModel.fromJson(f as Map<String, dynamic>)) .map((f) => ProjectFileModel.fromJson(f as Map<String, dynamic>))
.toList() .toList()
: [], : [],
); );
@@ -127,7 +129,7 @@ class SampleProjectModel {
link: entity.viewUrl, link: entity.viewUrl,
thumbnail: entity.thumbnailUrl, thumbnail: entity.thumbnailUrl,
filesList: entity.filesList filesList: entity.filesList
.map((f) => SampleProjectFileModel(name: f.id, fileUrl: f.fileUrl)) .map((f) => ProjectFileModel(name: f.id, fileUrl: f.fileUrl))
.toList(), .toList(),
); );
} }

View File

@@ -0,0 +1,41 @@
/// Design Request Repository Implementation
///
/// Implements the design request repository interface.
library;
import 'package:worker/features/showrooms/data/datasources/design_request_remote_datasource.dart';
import 'package:worker/features/showrooms/domain/entities/design_request.dart';
import 'package:worker/features/showrooms/domain/repositories/design_request_repository.dart';
/// Design Request Repository Implementation
class DesignRequestRepositoryImpl implements DesignRequestRepository {
const DesignRequestRepositoryImpl(this._remoteDataSource);
final DesignRequestRemoteDataSource _remoteDataSource;
@override
Future<List<DesignRequest>> getDesignRequests({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
final models = await _remoteDataSource.getDesignRequests(
limitStart: limitStart,
limitPageLength: limitPageLength,
);
return models.map((model) => model.toEntity()).toList();
} catch (e) {
rethrow;
}
}
@override
Future<DesignRequest> getDesignRequestDetail(String name) async {
try {
final model = await _remoteDataSource.getDesignRequestDetail(name);
return model.toEntity();
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,118 @@
/// Domain Entity: Design Request
///
/// Represents a design request/ticket submitted by user.
/// Based on API response from building_material.building_material.api.design_request
library;
import 'package:equatable/equatable.dart';
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
/// Design Request Status
///
/// Maps from API status_color field:
/// - "Success" -> completed
/// - "Warning" -> pending
/// - "Danger" -> rejected
/// - Other -> designing
enum DesignRequestStatus {
pending,
designing,
completed,
rejected,
}
/// Design Request Entity
///
/// Contains information about a design request ticket.
/// API field mapping:
/// - name -> id
/// - subject -> subject
/// - description -> description (HTML content)
/// - dateline -> dateline
/// - status -> statusText
/// - status_color -> statusColor (mapped to enum)
/// - files_list -> filesList (detail only)
class DesignRequest extends Equatable {
/// Unique request identifier (API: name)
final String id;
/// Request subject/title (API: subject)
final String subject;
/// Request description - may contain HTML (API: description)
final String? description;
/// Deadline date string (API: dateline)
final String? dateline;
/// Status display text (API: status)
final String statusText;
/// Status color code (API: status_color)
final String statusColor;
/// List of attached files (API: files_list) - available in detail
final List<ProjectFile> filesList;
const DesignRequest({
required this.id,
required this.subject,
this.description,
this.dateline,
required this.statusText,
required this.statusColor,
this.filesList = const [],
});
/// Get status enum from statusColor
DesignRequestStatus get status {
switch (statusColor.toLowerCase()) {
case 'success':
return DesignRequestStatus.completed;
case 'warning':
return DesignRequestStatus.pending;
case 'danger':
return DesignRequestStatus.rejected;
default:
return DesignRequestStatus.designing;
}
}
/// Check if request is completed
bool get isCompleted => status == DesignRequestStatus.completed;
/// Check if request is pending
bool get isPending => status == DesignRequestStatus.pending;
/// Check if request is rejected
bool get isRejected => status == DesignRequestStatus.rejected;
/// Get plain text description (strips HTML tags)
String get plainDescription {
if (description == null) return '';
// Simple HTML tag removal
return description!
.replaceAll(RegExp(r'<[^>]*>'), '')
.replaceAll('&nbsp;', ' ')
.trim();
}
/// Get all file URLs
List<String> get fileUrls => filesList.map((f) => f.fileUrl).toList();
@override
List<Object?> get props => [
id,
subject,
description,
dateline,
statusText,
statusColor,
filesList,
];
@override
String toString() {
return 'DesignRequest(id: $id, subject: $subject, status: $statusText, filesCount: ${filesList.length})';
}
}

View File

@@ -8,15 +8,21 @@ import 'package:equatable/equatable.dart';
/// Project File Entity /// Project File Entity
/// ///
/// Represents an uploaded file attached to a sample project. /// Shared entity for file attachments used by:
class SampleProjectFile extends Equatable { /// - SampleProject (model houses)
/// - DesignRequest (design requests)
///
/// API field mapping:
/// - name -> id
/// - file_url -> fileUrl
class ProjectFile extends Equatable {
/// Unique file identifier (API: name) /// Unique file identifier (API: name)
final String id; final String id;
/// Full URL to the file (API: file_url) /// Full URL to the file (API: file_url)
final String fileUrl; final String fileUrl;
const SampleProjectFile({ const ProjectFile({
required this.id, required this.id,
required this.fileUrl, required this.fileUrl,
}); });
@@ -52,7 +58,7 @@ class SampleProject extends Equatable {
final String? thumbnailUrl; final String? thumbnailUrl;
/// List of attached files/images (API: files_list) - available in detail /// List of attached files/images (API: files_list) - available in detail
final List<SampleProjectFile> filesList; final List<ProjectFile> filesList;
const SampleProject({ const SampleProject({
required this.id, required this.id,

View File

@@ -0,0 +1,26 @@
/// Design Request Repository Interface
///
/// Defines contract for design request data operations.
library;
import 'package:worker/features/showrooms/domain/entities/design_request.dart';
/// Design Request Repository
///
/// Repository interface for design request operations.
abstract class DesignRequestRepository {
/// Get list of design requests
///
/// Returns list of design requests.
/// [limitStart] - Pagination offset
/// [limitPageLength] - Number of items per page (0 = all)
Future<List<DesignRequest>> getDesignRequests({
int limitStart = 0,
int limitPageLength = 0,
});
/// Get detail of a design request by name
///
/// Returns full design request detail with files_list.
Future<DesignRequest> getDesignRequestDetail(String name);
}

View File

@@ -442,7 +442,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
void _showImageViewer( void _showImageViewer(
BuildContext context, BuildContext context,
List<SampleProjectFile> images, List<ProjectFile> images,
int initialIndex, int initialIndex,
) { ) {
showDialog<void>( showDialog<void>(
@@ -465,7 +465,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
/// Image Viewer Dialog with Swipe Navigation /// Image Viewer Dialog with Swipe Navigation
class _ImageViewerDialog extends StatefulWidget { class _ImageViewerDialog extends StatefulWidget {
final List<SampleProjectFile> images; final List<ProjectFile> images;
final int initialIndex; final int initialIndex;
const _ImageViewerDialog({ const _ImageViewerDialog({

View File

@@ -10,7 +10,9 @@ import 'package:go_router/go_router.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/showrooms/domain/entities/design_request.dart';
import 'package:worker/features/showrooms/domain/entities/sample_project.dart'; import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
import 'package:worker/features/showrooms/presentation/providers/design_request_provider.dart';
import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart'; import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart';
/// Model Houses Page /// Model Houses Page
@@ -370,90 +372,118 @@ class _LibraryCard extends StatelessWidget {
} }
/// Design Requests Tab /// Design Requests Tab
class _DesignRequestsTab extends StatelessWidget { class _DesignRequestsTab extends ConsumerWidget {
const _DesignRequestsTab(); const _DesignRequestsTab();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return ListView( final requestsAsync = ref.watch(designRequestsListProvider);
padding: const EdgeInsets.all(20),
children: const [ return requestsAsync.when(
_RequestCard( data: (requests) {
code: '#YC001', if (requests.isEmpty) {
status: DesignRequestStatus.completed, return const Center(
date: '20/10/2024', child: Padding(
description: 'Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)', padding: EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.design_services_outlined,
size: 64,
color: AppColors.grey500,
), ),
_RequestCard( SizedBox(height: 16),
code: '#YC002', Text(
status: DesignRequestStatus.designing, 'Chưa có yêu cầu thiết kế nào',
date: '25/10/2024', style: TextStyle(
description: 'Cải tạo căn hộ chung cư - Chị Lan (Quận 2)', fontSize: 16,
color: AppColors.grey500,
), ),
_RequestCard(
code: '#YC003',
status: DesignRequestStatus.pending,
date: '28/10/2024',
description: 'Thiết kế biệt thự 2 tầng - Anh Đức (Bình Dương)',
),
_RequestCard(
code: '#YC004',
status: DesignRequestStatus.pending,
date: '01/11/2024',
description: 'Thiết kế cửa hàng kinh doanh - Chị Mai (Quận 1)',
), ),
], ],
),
),
);
}
return RefreshIndicator(
onRefresh: () => ref.read(designRequestsListProvider.notifier).refresh(),
child: ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: requests.length,
itemBuilder: (context, index) {
final request = requests[index];
return _RequestCard(request: request);
},
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stack) => Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: AppColors.danger,
),
const SizedBox(height: 16),
Text(
'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.invalidate(designRequestsListProvider),
child: const Text('Thử lại'),
),
],
),
),
),
); );
} }
} }
/// Design Request Status
enum DesignRequestStatus { pending, designing, completed }
/// Request Card Widget /// Request Card Widget
class _RequestCard extends StatelessWidget { class _RequestCard extends StatelessWidget {
const _RequestCard({ const _RequestCard({required this.request});
required this.code,
required this.status,
required this.date,
required this.description,
});
final String code; final DesignRequest request;
final DesignRequestStatus status;
final String date;
final String description;
Color _getStatusColor() { Color _getStatusColor() {
switch (status) { switch (request.status) {
case DesignRequestStatus.pending: case DesignRequestStatus.pending:
return const Color(0xFFffc107); // Warning yellow return const Color(0xFFffc107); // Warning yellow
case DesignRequestStatus.designing: case DesignRequestStatus.designing:
return const Color(0xFF3730a3); // Indigo return const Color(0xFF3730a3); // Indigo
case DesignRequestStatus.completed: case DesignRequestStatus.completed:
return const Color(0xFF065f46); // Success green return const Color(0xFF065f46); // Success green
case DesignRequestStatus.rejected:
return const Color(0xFFdc2626); // Danger red
} }
} }
Color _getStatusBackgroundColor() { Color _getStatusBackgroundColor() {
switch (status) { switch (request.status) {
case DesignRequestStatus.pending: case DesignRequestStatus.pending:
return const Color(0xFFfef3c7); // Light yellow return const Color(0xFFfef3c7); // Light yellow
case DesignRequestStatus.designing: case DesignRequestStatus.designing:
return const Color(0xFFe0e7ff); // Light indigo return const Color(0xFFe0e7ff); // Light indigo
case DesignRequestStatus.completed: case DesignRequestStatus.completed:
return const Color(0xFFd1fae5); // Light green return const Color(0xFFd1fae5); // Light green
} case DesignRequestStatus.rejected:
} return const Color(0xFFfee2e2); // Light red
String _getStatusText() {
switch (status) {
case DesignRequestStatus.pending:
return 'CHỜ TIẾP NHẬN';
case DesignRequestStatus.designing:
return 'ĐANG THIẾT KẾ';
case DesignRequestStatus.completed:
return 'HOÀN THÀNH';
} }
} }
@@ -465,9 +495,7 @@ class _RequestCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.push( context.push('/model-houses/design-request/${request.id}');
'/model-houses/design-request/${code.replaceAll('#', '')}',
);
}, },
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Padding( child: Padding(
@@ -479,14 +507,18 @@ class _RequestCard extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Expanded(
'Mã yêu cầu: $code', child: Text(
'Mã yêu cầu: #${request.id}',
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.grey900, color: AppColors.grey900,
), ),
overflow: TextOverflow.ellipsis,
), ),
),
const SizedBox(width: 8),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
@@ -497,7 +529,7 @@ class _RequestCard extends StatelessWidget {
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Text( child: Text(
_getStatusText(), request.statusText.toUpperCase(),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -511,19 +543,35 @@ class _RequestCard extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
// Date // Date
if (request.dateline != null)
Text( Text(
'Ngày gửi: $date', 'Deadline: ${request.dateline}',
style: const TextStyle(fontSize: 14, color: AppColors.grey500), style: const TextStyle(fontSize: 14, color: AppColors.grey500),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Subject
Text(
request.subject,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
if (request.plainDescription.isNotEmpty) ...[
const SizedBox(height: 4),
// Description // Description
Text( Text(
description, request.plainDescription,
style: const TextStyle(fontSize: 14, color: AppColors.grey900), style: const TextStyle(fontSize: 14, color: AppColors.grey500),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
], ],
],
), ),
), ),
), ),

View File

@@ -0,0 +1,58 @@
/// Providers: Design Request
///
/// Riverpod providers for managing design request state.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/features/showrooms/data/datasources/design_request_remote_datasource.dart';
import 'package:worker/features/showrooms/data/repositories/design_request_repository_impl.dart';
import 'package:worker/features/showrooms/domain/entities/design_request.dart';
import 'package:worker/features/showrooms/domain/repositories/design_request_repository.dart';
part 'design_request_provider.g.dart';
/// Design Request Remote Data Source Provider
@riverpod
Future<DesignRequestRemoteDataSource> designRequestRemoteDataSource(Ref ref) async {
final dioClient = await ref.watch(dioClientProvider.future);
return DesignRequestRemoteDataSourceImpl(dioClient);
}
/// Design Request Repository Provider
@riverpod
Future<DesignRequestRepository> designRequestRepository(Ref ref) async {
final remoteDataSource = await ref.watch(designRequestRemoteDataSourceProvider.future);
return DesignRequestRepositoryImpl(remoteDataSource);
}
/// Design Requests List Provider
///
/// Fetches and manages design requests from API.
@riverpod
class DesignRequestsList extends _$DesignRequestsList {
@override
Future<List<DesignRequest>> build() async {
final repository = await ref.watch(designRequestRepositoryProvider.future);
return repository.getDesignRequests();
}
/// Refresh design requests from remote
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = await ref.read(designRequestRepositoryProvider.future);
return repository.getDesignRequests();
});
}
}
/// Design Request Detail Provider
///
/// Fetches detail of a specific design request by name.
/// Uses family modifier to cache by request name.
@riverpod
Future<DesignRequest> designRequestDetail(Ref ref, String name) async {
final repository = await ref.watch(designRequestRepositoryProvider.future);
return repository.getDesignRequestDetail(name);
}

View File

@@ -0,0 +1,266 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'design_request_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Design Request Remote Data Source Provider
@ProviderFor(designRequestRemoteDataSource)
const designRequestRemoteDataSourceProvider =
DesignRequestRemoteDataSourceProvider._();
/// Design Request Remote Data Source Provider
final class DesignRequestRemoteDataSourceProvider
extends
$FunctionalProvider<
AsyncValue<DesignRequestRemoteDataSource>,
DesignRequestRemoteDataSource,
FutureOr<DesignRequestRemoteDataSource>
>
with
$FutureModifier<DesignRequestRemoteDataSource>,
$FutureProvider<DesignRequestRemoteDataSource> {
/// Design Request Remote Data Source Provider
const DesignRequestRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'designRequestRemoteDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$designRequestRemoteDataSourceHash();
@$internal
@override
$FutureProviderElement<DesignRequestRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<DesignRequestRemoteDataSource> create(Ref ref) {
return designRequestRemoteDataSource(ref);
}
}
String _$designRequestRemoteDataSourceHash() =>
r'fb33861da35c711e637f01b182e81263345980fa';
/// Design Request Repository Provider
@ProviderFor(designRequestRepository)
const designRequestRepositoryProvider = DesignRequestRepositoryProvider._();
/// Design Request Repository Provider
final class DesignRequestRepositoryProvider
extends
$FunctionalProvider<
AsyncValue<DesignRequestRepository>,
DesignRequestRepository,
FutureOr<DesignRequestRepository>
>
with
$FutureModifier<DesignRequestRepository>,
$FutureProvider<DesignRequestRepository> {
/// Design Request Repository Provider
const DesignRequestRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'designRequestRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$designRequestRepositoryHash();
@$internal
@override
$FutureProviderElement<DesignRequestRepository> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<DesignRequestRepository> create(Ref ref) {
return designRequestRepository(ref);
}
}
String _$designRequestRepositoryHash() =>
r'c1f68c7c45d8148871882086d3727272c194934d';
/// Design Requests List Provider
///
/// Fetches and manages design requests from API.
@ProviderFor(DesignRequestsList)
const designRequestsListProvider = DesignRequestsListProvider._();
/// Design Requests List Provider
///
/// Fetches and manages design requests from API.
final class DesignRequestsListProvider
extends $AsyncNotifierProvider<DesignRequestsList, List<DesignRequest>> {
/// Design Requests List Provider
///
/// Fetches and manages design requests from API.
const DesignRequestsListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'designRequestsListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$designRequestsListHash();
@$internal
@override
DesignRequestsList create() => DesignRequestsList();
}
String _$designRequestsListHash() =>
r'368656997bd73619c7b27a3923066149a403bb5f';
/// Design Requests List Provider
///
/// Fetches and manages design requests from API.
abstract class _$DesignRequestsList
extends $AsyncNotifier<List<DesignRequest>> {
FutureOr<List<DesignRequest>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref as $Ref<AsyncValue<List<DesignRequest>>, List<DesignRequest>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<DesignRequest>>, List<DesignRequest>>,
AsyncValue<List<DesignRequest>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Design Request Detail Provider
///
/// Fetches detail of a specific design request by name.
/// Uses family modifier to cache by request name.
@ProviderFor(designRequestDetail)
const designRequestDetailProvider = DesignRequestDetailFamily._();
/// Design Request Detail Provider
///
/// Fetches detail of a specific design request by name.
/// Uses family modifier to cache by request name.
final class DesignRequestDetailProvider
extends
$FunctionalProvider<
AsyncValue<DesignRequest>,
DesignRequest,
FutureOr<DesignRequest>
>
with $FutureModifier<DesignRequest>, $FutureProvider<DesignRequest> {
/// Design Request Detail Provider
///
/// Fetches detail of a specific design request by name.
/// Uses family modifier to cache by request name.
const DesignRequestDetailProvider._({
required DesignRequestDetailFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'designRequestDetailProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$designRequestDetailHash();
@override
String toString() {
return r'designRequestDetailProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<DesignRequest> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<DesignRequest> create(Ref ref) {
final argument = this.argument as String;
return designRequestDetail(ref, argument);
}
@override
bool operator ==(Object other) {
return other is DesignRequestDetailProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$designRequestDetailHash() =>
r'ddf1fdd91e1e9dc15acf50ef69d85602f02041c6';
/// Design Request Detail Provider
///
/// Fetches detail of a specific design request by name.
/// Uses family modifier to cache by request name.
final class DesignRequestDetailFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<DesignRequest>, String> {
const DesignRequestDetailFamily._()
: super(
retry: null,
name: r'designRequestDetailProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Design Request Detail Provider
///
/// Fetches detail of a specific design request by name.
/// Uses family modifier to cache by request name.
DesignRequestDetailProvider call(String name) =>
DesignRequestDetailProvider._(argument: name, from: this);
@override
String toString() => r'designRequestDetailProvider';
}