Compare commits

...

4 Commits

Author SHA1 Message Date
Phuoc Nguyen
9e7bda32f2 request detail 2025-11-28 15:47:51 +07:00
Phuoc Nguyen
65f6f825a6 update md 2025-11-28 15:16:40 +07:00
Phuoc Nguyen
440b474504 sample project 2025-11-28 15:01:51 +07:00
Phuoc Nguyen
ed6cc4cebc add dleete image projects 2025-11-28 13:47:47 +07:00
42 changed files with 2747 additions and 1083 deletions

View File

@@ -139,6 +139,14 @@ curl --location 'https://land.dbiz.com//api/method/upload_file' \
--form 'docname="p9ti8veq2g"' \
--form 'optimize="true"'
#delete image file of project
curl --location 'https://land.dbiz.com//api/method/frappe.desk.form.utils.remove_attach' \
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
--form 'fid="67803d2e95"' \ #file id to be deleted
--form 'dt="Architectural Project"' \ #doctye
--form 'dn="p9ti8veq2g"' #docname
#get detail of a project
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_detail' \
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \

73
docs/request.sh Normal file
View File

@@ -0,0 +1,73 @@
#get list
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.design_request.get_list' \
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
--header 'Content-Type: application/json' \
--data '{
"limit_start": 0,
"limit_page_length": 0
}'
#response
{
"message": [
{
"name": "ISS-2025-00005",
"subject": "Nhà phố 2 tầng",
"description": "<div class=\"ql-editor read-mode\"><p>Diện tích: 150m2</p><p>Khu vực: Quận 1, TP.HCM</p><p>Phong cách mong muốn: Hiện đại</p><p>Ngân sách dự kiến: 500 triệu</p><p>Yêu cầu chi tiết: Cần thiết kế phòng khách rộng, 3 phòng ngủ</p></div>",
"dateline": "2025-12-31",
"status": "Từ chối",
"status_color": "Danger"
},
{
"name": "ISS-2025-00004",
"subject": "Nhà phố 2 tầng",
"description": "<div class=\"ql-editor read-mode\"><p>Diện tích: 150</p><p>Khu vực: Quận 1, TP.HCM</p><p>Phong cách mong muốn: Hiện đại</p><p>Ngân sách dự kiến: 500 triệu</p><p>Yêu cầu chi tiết: Cần thiết kế phòng khách rộng, 3 phòng ngủ</p></div>",
"dateline": "2025-12-31",
"status": "Chờ phê duyệt",
"status_color": "Warning"
},
{
"name": "ISS-2025-00003",
"subject": "Nhà phố 2 tầng",
"description": "<div class=\"ql-editor read-mode\"><p>Diện tích: 150 m²</p><p>Khu vực: Quận 1, TP.HCM</p><p>Phong cách mong muốn: Hiện đại</p><p>Ngân sách dự kiến: 500 triệu</p><p>Yêu cầu chi tiết: Cần thiết kế phòng khách rộng, 3 phòng ngủ</p></div>",
"dateline": "2025-12-31",
"status": "Chờ phê duyệt",
"status_color": "Warning"
},
{
"name": "ISS-2025-00002",
"subject": "Nhà phố 2 tầng",
"description": "<div class=\"ql-editor read-mode\"><p>Diện tích: 150 m²</p><p>Khu vực: Quận 1, TP.HCM</p><p>Phong cách mong muốn: Hiện đại</p><p>Ngân sách dự kiến: 500 triệu</p><p>Yêu cầu chi tiết: Cần thiết kế phòng khách rộng, 3 phòng ngủ</p></div>",
"dateline": "2025-12-31",
"status": "Hoàn thành",
"status_color": "Success"
}
]
}
#get detail
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.design_request.get_detail' \
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
--header 'Content-Type: application/json' \
--data '{
"name" : "ISS-2025-00005"
}'
#response
{
"message": {
"name": "ISS-2025-00005",
"subject": "Nhà phố 2 tầng",
"description": "<div class=\"ql-editor read-mode\"><p>Diện tích: 150m2</p><p>Khu vực: Quận 1, TP.HCM</p><p>Phong cách mong muốn: Hiện đại</p><p>Ngân sách dự kiến: 500 triệu</p><p>Yêu cầu chi tiết: Cần thiết kế phòng khách rộng, 3 phòng ngủ</p></div>",
"dateline": "2025-12-31",
"status": "Từ chối",
"status_color": "Danger",
"files_list": [
{
"name": "433f777958",
"file_url": "https://land.dbiz.com/files/b0d6423a04ce8890d1df.jpg"
}
]
}
}

61
docs/sample_project.sh Normal file
View File

@@ -0,0 +1,61 @@
#get list
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sample_project.get_list' \
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
--header 'Content-Type: application/json' \
--data '{
"limit_page_length" : 0,
"limit_start" : 0
}'
#response
{
"message": [
{
"name": "PROJ-0001",
"project_name": "Căn hộ Studio",
"notes": "<div class=\"ql-editor read-mode\"><p>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. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.</p></div>",
"link": "https://vr.house3d.com/web/panorama-player/H00179549",
"thumbnail": "https://land.dbiz.com//private/files/photo-1600596542815-ffad4c1539a9.jpg"
}
]
}
#GET DETAIL OF A SAMPLE PROJECT
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sample_project.get_detail' \
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
--header 'Content-Type: application/json' \
--data '{
"name" : "PROJ-0001"
}'
#RESPONSE
{
"message": {
"name": "PROJ-0001",
"project_name": "Căn hộ Studio",
"notes": "<div class=\"ql-editor read-mode\"><p>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. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.</p></div>",
"link": "https://vr.house3d.com/web/panorama-player/H00179549",
"thumbnail": "https://land.dbiz.com/private/files/photo-1600596542815-ffad4c1539a9.jpg",
"files_list": [
{
"name": "1fe604db77",
"file_url": "https://land.dbiz.com/private/files/photo-1600596542815-ffad4c1539a9.jpg"
},
{
"name": "0e3d2714ee",
"file_url": "https://land.dbiz.com/files/main_img.jpg"
},
{
"name": "fd7970daa3",
"file_url": "https://land.dbiz.com/files/project_img_0.jpg"
},
{
"name": "a42fbef956",
"file_url": "https://land.dbiz.com/files/project_img_1.jpg"
}
]
}
}

View File

@@ -316,13 +316,13 @@
</div>
<div class="library-content" onclick="viewLibraryDetail('studio-apartment')">
<h3 class="library-title">Căn hộ Studio</h3>
<div class="library-date">
<!--<div class="library-date">
<i class="fas fa-calendar-alt"></i>
<span>Ngày đăng: 15/11/2024</span>
</div>
<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.
</p>
</p>-->
</div>
</div>
@@ -336,13 +336,13 @@
</div>
<div class="library-content">
<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>
<span>Ngày đăng: 12/11/2024</span>
</div>
<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.
</p>
</p>-->
</div>
</div>
@@ -356,13 +356,13 @@
</div>
<div class="library-content">
<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>
<span>Ngày đăng: 08/11/2024</span>
</div>
<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.
</p>
</p>-->
</div>
</div>
@@ -376,13 +376,13 @@
</div>
<div class="library-content">
<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>
<span>Ngày đăng: 05/11/2024</span>
</div>
<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.
</p>
</p>-->
</div>
</div>
</div>

View File

@@ -315,6 +315,47 @@ class ApiConstants {
static const String getProjectDetail =
'/building_material.building_material.api.project.get_detail';
/// Delete project file/attachment (requires sid and csrf_token)
/// POST /api/method/frappe.desk.form.utils.remove_attach
/// Form-data: { "fid": "file_id", "dt": "Architectural Project", "dn": "project_name" }
static const String removeProjectFile = '/frappe.desk.form.utils.remove_attach';
// ============================================================================
// Sample Project / Model House Endpoints (Frappe ERPNext)
// ============================================================================
/// Get list of sample/model house projects (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.sample_project.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: { "message": [{ "name": "...", "project_name": "...", "notes": "...", "link": "...", "thumbnail": "..." }] }
static const String getSampleProjectList =
'/building_material.building_material.api.sample_project.get_list';
/// Get detail of a sample/model house project (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.sample_project.get_detail
/// Body: { "name": "PROJ-0001" }
/// Returns: { "message": { "name": "...", "project_name": "...", "notes": "...", "link": "...", "thumbnail": "...", "files_list": [...] } }
static const String getSampleProjectDetail =
'/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)
/// POST /projects
static const String createProject = '/projects';

View File

@@ -43,6 +43,14 @@ abstract class SubmissionsRemoteDataSource {
required String projectName,
required String filePath,
});
/// Delete a file from a project submission
/// [fileId] is the file ID to delete
/// [projectName] is the project name (docname)
Future<void> deleteProjectFile({
required String fileId,
required String projectName,
});
}
/// Submissions Remote Data Source Implementation
@@ -303,4 +311,41 @@ class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource {
throw Exception('Failed to upload project file: $e');
}
}
/// Delete a file from a project submission
///
/// Calls: POST /api/method/frappe.desk.form.utils.remove_attach
/// Form-data: fid, dt, dn
@override
Future<void> deleteProjectFile({
required String fileId,
required String projectName,
}) async {
try {
final formData = FormData.fromMap({
'fid': fileId,
'dt': 'Architectural Project',
'dn': projectName,
});
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.removeProjectFile}',
data: formData,
);
final data = response.data;
if (data == null) {
throw Exception('No data received from deleteProjectFile API');
}
// Check for error in response
if (data['exc_type'] != null || data['exception'] != null) {
final errorMessage =
data['_server_messages'] ?? data['exception'] ?? 'Unknown error';
throw Exception('API error: $errorMessage');
}
} catch (e) {
throw Exception('Failed to delete project file: $e');
}
}
}

View File

@@ -171,4 +171,19 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
rethrow;
}
}
@override
Future<void> deleteProjectFile({
required String fileId,
required String projectName,
}) async {
try {
return await _remoteDataSource.deleteProjectFile(
fileId: fileId,
projectName: projectName,
);
} catch (e) {
rethrow;
}
}
}

View File

@@ -54,4 +54,12 @@ abstract class SubmissionsRepository {
required String projectName,
required String filePath,
});
/// Delete a file from a project submission
/// [fileId] is the file ID to delete
/// [projectName] is the project name (docname)
Future<void> deleteProjectFile({
required String fileId,
required String projectName,
});
}

View File

@@ -51,6 +51,7 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
List<ProjectFile> _existingFiles = []; // Existing files from API
bool _isSubmitting = false;
bool _isLoadingDetail = false;
String? _deletingFileId; // Track which file is being deleted
/// Whether we're editing an existing submission
bool get isEditing => widget.submission != null;
@@ -858,17 +859,22 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
Widget _buildExistingFileItem(ProjectFile file, int index) {
final fileName = file.fileUrl.split('/').last;
final isDeleting = _deletingFileId == file.id;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
border: Border.all(color: AppColors.success),
border: Border.all(
color: isDeleting ? AppColors.grey500 : AppColors.success,
),
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
// Network image
// Network image with delete overlay
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
@@ -902,6 +908,28 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
),
),
),
// Deleting overlay
if (isDeleting)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(4),
),
child: const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
),
),
),
],
),
const SizedBox(width: 12),
Expanded(
child: Column(
@@ -909,25 +937,36 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
children: [
Text(
fileName,
style: const TextStyle(
style: TextStyle(
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: isDeleting ? AppColors.grey500 : AppColors.grey900,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
const Text(
'Đã tải lên',
Text(
isDeleting ? 'Đang xóa...' : 'Đã tải lên',
style: TextStyle(
fontSize: 12,
color: AppColors.success,
color: isDeleting ? AppColors.grey500 : AppColors.success,
),
),
],
),
),
// Checkmark icon
// Delete button or checkmark
if (!_isSubmitting && !isDeleting)
IconButton(
icon: const FaIcon(
FontAwesomeIcons.trash,
size: 16,
color: AppColors.danger,
),
onPressed: () => _showDeleteConfirmDialog(file),
tooltip: 'Xóa ảnh',
)
else if (!isDeleting)
const FaIcon(
FontAwesomeIcons.circleCheck,
size: 16,
@@ -938,6 +977,147 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
);
}
/// Show confirmation dialog before deleting an existing file
Future<void> _showDeleteConfirmDialog(ProjectFile file) async {
final fileName = file.fileUrl.split('/').last;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
FaIcon(
FontAwesomeIcons.triangleExclamation,
color: AppColors.danger,
size: 20,
),
SizedBox(width: 12),
Text('Xác nhận xóa'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Bạn có chắc chắn muốn xóa ảnh này?'),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.grey100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: file.fileUrl,
width: 48,
height: 48,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 48,
height: 48,
color: AppColors.grey100,
),
errorWidget: (context, url, error) => Container(
width: 48,
height: 48,
color: AppColors.grey100,
child: const Center(child: FaIcon(FontAwesomeIcons.image, size: 20)),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
fileName,
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
const Text(
'Hành động này không thể hoàn tác.',
style: TextStyle(
fontSize: 13,
color: AppColors.danger,
fontStyle: FontStyle.italic,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Hủy'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.danger,
foregroundColor: Colors.white,
),
child: const Text('Xóa'),
),
],
),
);
if (confirmed == true && mounted) {
await _deleteExistingFile(file);
}
}
/// Delete an existing file from the project
Future<void> _deleteExistingFile(ProjectFile file) async {
if (!isEditing || widget.submission == null) return;
setState(() => _deletingFileId = file.id);
try {
final repository = await ref.read(submissionsRepositoryProvider.future);
await repository.deleteProjectFile(
fileId: file.id,
projectName: widget.submission!.submissionId,
);
if (!mounted) return;
// Remove from local list
setState(() {
_existingFiles = _existingFiles.where((f) => f.id != file.id).toList();
_deletingFileId = null;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã xóa ảnh thành công'),
backgroundColor: AppColors.success,
),
);
} catch (e) {
if (mounted) {
setState(() => _deletingFileId = null);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Lỗi xóa ảnh: ${e.toString().replaceAll('Exception: ', '')}'),
backgroundColor: AppColors.danger,
),
);
}
}
}
Widget _buildSubmitButton() {
return SizedBox(
width: double.infinity,

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,96 @@
/// Sample Project Remote Data Source
///
/// Handles remote API calls for sample/model house projects.
library;
import 'package:worker/core/constants/api_constants.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/features/showrooms/data/models/sample_project_model.dart';
/// Sample Project Remote Data Source Interface
abstract class SampleProjectRemoteDataSource {
/// Fetch list of sample/model house projects from API
Future<List<SampleProjectModel>> getSampleProjects({
int limitStart = 0,
int limitPageLength = 0,
});
/// Fetch detail of a sample/model house project by name
Future<SampleProjectModel> getSampleProjectDetail(String name);
}
/// Sample Project Remote Data Source Implementation
class SampleProjectRemoteDataSourceImpl implements SampleProjectRemoteDataSource {
const SampleProjectRemoteDataSourceImpl(this._dioClient);
final DioClient _dioClient;
/// Get list of sample projects
///
/// Calls: POST /api/method/building_material.building_material.api.sample_project.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: List of sample projects with 360° view links
@override
Future<List<SampleProjectModel>> getSampleProjects({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getSampleProjectList}',
data: {
'limit_start': limitStart,
'limit_page_length': limitPageLength,
},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getSampleProjectList API');
}
// API returns: { "message": [...] }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getSampleProjectList response');
}
final List<dynamic> projectsList = message as List<dynamic>;
return projectsList
.map((json) => SampleProjectModel.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
throw Exception('Failed to get sample projects: $e');
}
}
/// Get detail of a sample project by name
///
/// Calls: POST /api/method/building_material.building_material.api.sample_project.get_detail
/// Body: { "name": "PROJ-0001" }
/// Returns: Full project detail with files_list
@override
Future<SampleProjectModel> getSampleProjectDetail(String name) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getSampleProjectDetail}',
data: {'name': name},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getSampleProjectDetail API');
}
// API returns: { "message": {...} }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getSampleProjectDetail response');
}
return SampleProjectModel.fromJson(message as Map<String, dynamic>);
} catch (e) {
throw Exception('Failed to get sample project 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

@@ -0,0 +1,136 @@
/// Data Model: Sample Project Model
///
/// JSON serialization model for sample project API responses.
library;
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
/// Project File Model
///
/// Shared model for file attachments used by:
/// - SampleProjectModel (model houses)
/// - DesignRequestModel (design requests)
class ProjectFileModel {
/// Unique file identifier (API: name)
final String name;
/// Full URL to the file (API: file_url)
final String fileUrl;
const ProjectFileModel({
required this.name,
required this.fileUrl,
});
/// Create model from JSON map
factory ProjectFileModel.fromJson(Map<String, dynamic> json) {
return ProjectFileModel(
name: json['name'] as String? ?? '',
fileUrl: json['file_url'] as String? ?? '',
);
}
/// Convert model to JSON map
Map<String, dynamic> toJson() {
return {
'name': name,
'file_url': fileUrl,
};
}
/// Convert to domain entity
ProjectFile toEntity() {
return ProjectFile(
id: name,
fileUrl: fileUrl,
);
}
}
/// Sample Project Model
///
/// Handles JSON serialization/deserialization for API communication.
class SampleProjectModel {
/// Unique project identifier (API: name)
final String name;
/// Project display name (API: project_name)
final String projectName;
/// Project description/notes - may contain HTML (API: notes)
final String? notes;
/// URL to 360° view (API: link)
final String? link;
/// Thumbnail image URL (API: thumbnail)
final String? thumbnail;
/// List of attached files/images (API: files_list) - available in detail
final List<ProjectFileModel> filesList;
const SampleProjectModel({
required this.name,
required this.projectName,
this.notes,
this.link,
this.thumbnail,
this.filesList = const [],
});
/// Create model from JSON map
factory SampleProjectModel.fromJson(Map<String, dynamic> json) {
final filesListJson = json['files_list'] as List<dynamic>?;
return SampleProjectModel(
name: json['name'] as String? ?? '',
projectName: json['project_name'] as String? ?? '',
notes: json['notes'] as String?,
link: json['link'] as String?,
thumbnail: json['thumbnail'] 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,
'project_name': projectName,
'notes': notes,
'link': link,
'thumbnail': thumbnail,
'files_list': filesList.map((f) => f.toJson()).toList(),
};
}
/// Convert to domain entity
SampleProject toEntity() {
return SampleProject(
id: name,
projectName: projectName,
description: notes,
viewUrl: link,
thumbnailUrl: thumbnail,
filesList: filesList.map((f) => f.toEntity()).toList(),
);
}
/// Create model from domain entity
factory SampleProjectModel.fromEntity(SampleProject entity) {
return SampleProjectModel(
name: entity.id,
projectName: entity.projectName,
notes: entity.description,
link: entity.viewUrl,
thumbnail: entity.thumbnailUrl,
filesList: entity.filesList
.map((f) => ProjectFileModel(name: f.id, fileUrl: f.fileUrl))
.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,41 @@
/// Sample Project Repository Implementation
///
/// Implements the sample project repository interface.
library;
import 'package:worker/features/showrooms/data/datasources/sample_project_remote_datasource.dart';
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
import 'package:worker/features/showrooms/domain/repositories/sample_project_repository.dart';
/// Sample Project Repository Implementation
class SampleProjectRepositoryImpl implements SampleProjectRepository {
const SampleProjectRepositoryImpl(this._remoteDataSource);
final SampleProjectRemoteDataSource _remoteDataSource;
@override
Future<List<SampleProject>> getSampleProjects({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
final models = await _remoteDataSource.getSampleProjects(
limitStart: limitStart,
limitPageLength: limitPageLength,
);
return models.map((model) => model.toEntity()).toList();
} catch (e) {
rethrow;
}
}
@override
Future<SampleProject> getSampleProjectDetail(String name) async {
try {
final model = await _remoteDataSource.getSampleProjectDetail(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

@@ -0,0 +1,95 @@
/// Domain Entity: Sample Project
///
/// Represents a sample/model house project with 360° view.
/// Based on API response from building_material.building_material.api.sample_project
library;
import 'package:equatable/equatable.dart';
/// Project File Entity
///
/// Shared entity for file attachments used by:
/// - SampleProject (model houses)
/// - DesignRequest (design requests)
///
/// API field mapping:
/// - name -> id
/// - file_url -> fileUrl
class ProjectFile extends Equatable {
/// Unique file identifier (API: name)
final String id;
/// Full URL to the file (API: file_url)
final String fileUrl;
const ProjectFile({
required this.id,
required this.fileUrl,
});
@override
List<Object?> get props => [id, fileUrl];
}
/// Sample Project Entity
///
/// Contains information about a model house/sample project.
/// API field mapping:
/// - name -> id
/// - project_name -> projectName
/// - notes -> description (HTML content)
/// - link -> viewUrl (360° viewer URL)
/// - thumbnail -> thumbnailUrl
/// - files_list -> filesList (detail only)
class SampleProject extends Equatable {
/// Unique project identifier (API: name)
final String id;
/// Project display name (API: project_name)
final String projectName;
/// Project description/notes - may contain HTML (API: notes)
final String? description;
/// URL to 360° view (API: link)
final String? viewUrl;
/// Thumbnail image URL (API: thumbnail)
final String? thumbnailUrl;
/// List of attached files/images (API: files_list) - available in detail
final List<ProjectFile> filesList;
const SampleProject({
required this.id,
required this.projectName,
this.description,
this.viewUrl,
this.thumbnailUrl,
this.filesList = const [],
});
/// Check if project has 360° view available
bool get has360View => viewUrl != null && viewUrl!.isNotEmpty;
/// 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 image URLs for gallery (from filesList)
List<String> get imageUrls => filesList.map((f) => f.fileUrl).toList();
@override
List<Object?> get props => [id, projectName, description, viewUrl, thumbnailUrl, filesList];
@override
String toString() {
return 'SampleProject(id: $id, projectName: $projectName, has360View: $has360View, filesCount: ${filesList.length})';
}
}

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

@@ -0,0 +1,26 @@
/// Sample Project Repository Interface
///
/// Defines contract for sample project data operations.
library;
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
/// Sample Project Repository
///
/// Repository interface for sample/model house project operations.
abstract class SampleProjectRepository {
/// Get list of sample/model house projects
///
/// Returns list of sample projects with 360° view links.
/// [limitStart] - Pagination offset
/// [limitPageLength] - Number of items per page (0 = all)
Future<List<SampleProject>> getSampleProjects({
int limitStart = 0,
int limitPageLength = 0,
});
/// Get detail of a sample/model house project by name
///
/// Returns full project detail with files_list for gallery.
Future<SampleProject> getSampleProjectDetail(String name);
}

View File

@@ -12,6 +12,8 @@ import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart';
/// Model House Detail Page
class ModelHouseDetailPage extends ConsumerWidget {
@@ -24,8 +26,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Mock data - in real app, fetch from provider
final modelData = _getMockData(modelId);
final detailAsync = ref.watch(sampleProjectDetailProvider(modelId));
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
@@ -43,13 +44,16 @@ class ModelHouseDetailPage extends ConsumerWidget {
style: TextStyle(color: Colors.black),
),
actions: [
IconButton(
detailAsync.maybeWhen(
data: (project) => IconButton(
icon: const FaIcon(
FontAwesomeIcons.shareNodes,
color: Colors.black,
size: 20,
),
onPressed: () => _shareModel(context, modelData),
onPressed: () => _shareModel(context, project),
),
orElse: () => const SizedBox.shrink(),
),
const SizedBox(width: AppSpacing.sm),
],
@@ -57,34 +61,69 @@ class ModelHouseDetailPage extends ConsumerWidget {
backgroundColor: AppColors.white,
centerTitle: false,
),
body: SingleChildScrollView(
body: detailAsync.when(
data: (project) => SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 360° View Launcher
_build360ViewLauncher(context, modelData),
_build360ViewLauncher(context, project),
const SizedBox(height: 16),
// Project Information
_buildProjectInfo(modelData),
_buildProjectInfo(project),
const SizedBox(height: 16),
// Image Gallery
_buildImageGallery(context, modelData),
if (project.filesList.isNotEmpty)
_buildImageGallery(context, project),
const SizedBox(height: 40),
],
),
),
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(sampleProjectDetailProvider(modelId)),
child: const Text('Thử lại'),
),
],
),
),
),
),
);
}
Widget _build360ViewLauncher(
BuildContext context,
Map<String, dynamic> modelData,
) {
Widget _build360ViewLauncher(BuildContext context, SampleProject project) {
final hasImages = project.filesList.isNotEmpty;
final firstImageUrl = hasImages ? project.filesList.first.fileUrl : project.thumbnailUrl;
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
@@ -100,7 +139,9 @@ class ModelHouseDetailPage extends ConsumerWidget {
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _launch360View(context, modelData['url360'] as String),
onTap: project.has360View
? () => _launch360View(context, project.viewUrl!)
: null,
borderRadius: BorderRadius.circular(12),
child: Container(
height: 400,
@@ -115,15 +156,16 @@ class ModelHouseDetailPage extends ConsumerWidget {
child: Stack(
children: [
// Background image with overlay
if (firstImageUrl != null)
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Opacity(
opacity: 0.3,
child: CachedNetworkImage(
imageUrl: (modelData['images'] as List<Map<String, String>>)
.first['url']!,
imageUrl: firstImageUrl,
fit: BoxFit.cover,
errorWidget: (context, url, error) => const SizedBox.shrink(),
),
),
),
@@ -190,6 +232,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
),
const SizedBox(height: 20),
// Launch Button
if (project.has360View)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 32,
@@ -225,6 +268,25 @@ class ModelHouseDetailPage extends ConsumerWidget {
),
],
),
)
else
Container(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(24),
),
child: const Text(
'Chưa có view 360°',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
],
),
@@ -237,7 +299,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
);
}
Widget _buildProjectInfo(Map<String, dynamic> modelData) {
Widget _buildProjectInfo(SampleProject project) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(20),
@@ -257,45 +319,18 @@ class ModelHouseDetailPage extends ConsumerWidget {
children: [
// Title
Text(
modelData['title'] as String,
project.projectName,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
if (project.plainDescription.isNotEmpty) ...[
const SizedBox(height: 16),
// Specs Grid
Row(
children: [
Expanded(
child: _buildSpecItem(
'Diện tích',
modelData['area'] as String,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSpecItem(
'Địa điểm',
modelData['location'] as String,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSpecItem(
'Phong cách',
modelData['style'] as String,
),
),
],
),
const SizedBox(height: 20),
// Description
Text(
modelData['description'] as String,
project.plainDescription,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF4b5563),
@@ -303,48 +338,13 @@ class ModelHouseDetailPage extends ConsumerWidget {
),
),
],
),
);
}
Widget _buildSpecItem(String label, String value) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(
label.toUpperCase(),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.grey500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildImageGallery(
BuildContext context,
Map<String, dynamic> modelData,
) {
final images = modelData['images'] as List<Map<String, String>>;
Widget _buildImageGallery(BuildContext context, SampleProject project) {
final images = project.filesList;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
@@ -364,17 +364,17 @@ class ModelHouseDetailPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Gallery Title
const Row(
Row(
children: [
FaIcon(
const FaIcon(
FontAwesomeIcons.images,
size: 18,
color: AppColors.grey900,
),
SizedBox(width: 8),
const SizedBox(width: 8),
Text(
'Thư viện Hình ảnh',
style: TextStyle(
'Thư viện Hình ảnh (${images.length})',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
@@ -402,7 +402,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
width: 120,
height: 120,
child: CachedNetworkImage(
imageUrl: image['url']!,
imageUrl: image.fileUrl,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: AppColors.grey100,
@@ -442,7 +442,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
void _showImageViewer(
BuildContext context,
List<Map<String, String>> images,
List<ProjectFile> images,
int initialIndex,
) {
showDialog<void>(
@@ -455,62 +455,17 @@ class ModelHouseDetailPage extends ConsumerWidget {
);
}
void _shareModel(BuildContext context, Map<String, dynamic> modelData) {
Share.share(
'Xem mô hình 360° ${modelData['title']}\n${modelData['url360']}',
subject: modelData['title'] as String,
);
}
Map<String, dynamic> _getMockData(String modelId) {
// Mock data - in real app, fetch from repository
return {
'title': 'Căn hộ Studio',
'area': '35m²',
'location': 'Quận 7',
'style': 'Hiện đại',
'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. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.',
'url360': 'https://vr.house3d.com/web/panorama-player/H00179549',
'images': [
{
'url':
'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=600&fit=crop',
'caption': 'Phối cảnh tổng thể căn hộ studio với thiết kế hiện đại',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/main_img.jpg',
'caption': 'Khu vực phòng khách với gạch granite cao cấp',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_1.jpg?v=1',
'caption': 'Phòng ngủ chính với gạch ceramic màu trung tính',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_0.jpg?v=1',
'caption': 'Khu vực bếp với gạch mosaic điểm nhấn',
},
{
'url':
'https://images.unsplash.com/photo-1620626011761-996317b8d101?w=800&h=600&fit=crop',
'caption': 'Phòng tắm hiện đại với gạch chống thấm cao cấp',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_3.jpg?v=1',
'caption': 'Khu vực bàn ăn ấm cúng',
},
],
};
void _shareModel(BuildContext context, SampleProject project) {
final shareText = project.has360View
? 'Xem mô hình 360° ${project.projectName}\n${project.viewUrl}'
: 'Xem nhà mẫu ${project.projectName}';
SharePlus.instance.share(ShareParams(text: shareText));
}
}
/// Image Viewer Dialog with Swipe Navigation
class _ImageViewerDialog extends StatefulWidget {
final List<Map<String, String>> images;
final List<ProjectFile> images;
final int initialIndex;
const _ImageViewerDialog({
@@ -561,7 +516,7 @@ class _ImageViewerDialogState extends State<_ImageViewerDialog> {
itemBuilder: (context, index) {
return Center(
child: CachedNetworkImage(
imageUrl: widget.images[index]['url']!,
imageUrl: widget.images[index].fileUrl,
fit: BoxFit.contain,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(color: Colors.white),
@@ -618,34 +573,6 @@ class _ImageViewerDialogState extends State<_ImageViewerDialog> {
),
),
),
// Caption at bottom
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.7),
],
),
),
child: Text(
widget.images[_currentIndex]['caption']!,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
),
],
),
),

View File

@@ -10,6 +10,10 @@ import 'package:go_router/go_router.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.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/presentation/providers/design_request_provider.dart';
import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart';
/// Model Houses Page
///
@@ -160,76 +164,94 @@ class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
}
/// Library Tab - Model house 360° library
class _LibraryTab extends StatelessWidget {
class _LibraryTab extends ConsumerWidget {
const _LibraryTab();
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(20),
children: const [
_LibraryCard(
modelId: 'studio-01',
imageUrl:
'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=200&fit=crop',
title: 'Căn hộ Studio',
date: '15/11/2024',
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.',
has360View: true,
Widget build(BuildContext context, WidgetRef ref) {
final sampleProjectsAsync = ref.watch(sampleProjectsListProvider);
return sampleProjectsAsync.when(
data: (projects) {
if (projects.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.home_work_outlined,
size: 64,
color: AppColors.grey500,
),
_LibraryCard(
modelId: 'villa-01',
imageUrl:
'https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=200&fit=crop',
title: 'Biệt thự Hiện đại',
date: '12/11/2024',
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.',
has360View: true,
SizedBox(height: 16),
Text(
'Chưa có mẫu nhà nào',
style: TextStyle(
fontSize: 16,
color: AppColors.grey500,
),
_LibraryCard(
modelId: 'townhouse-01',
imageUrl:
'https://images.unsplash.com/photo-1562663474-6cbb3eaa4d14?w=800&h=200&fit=crop',
title: 'Nhà phố Tối giản',
date: '08/11/2024',
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.',
has360View: true,
),
_LibraryCard(
modelId: 'apartment-01',
imageUrl:
'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&h=200&fit=crop',
title: 'Chung cư Cao cấp',
date: '05/11/2024',
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.',
has360View: true,
),
],
),
),
);
}
return RefreshIndicator(
onRefresh: () => ref.read(sampleProjectsListProvider.notifier).refresh(),
child: ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: projects.length,
itemBuilder: (context, index) {
final project = projects[index];
return _LibraryCard(project: project);
},
),
);
},
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(sampleProjectsListProvider),
child: const Text('Thử lại'),
),
],
),
),
),
);
}
}
/// Library Card Widget
class _LibraryCard extends StatelessWidget {
const _LibraryCard({
required this.modelId,
required this.imageUrl,
required this.title,
required this.date,
required this.description,
this.has360View = false,
});
const _LibraryCard({required this.project});
final String modelId;
final String imageUrl;
final String title;
final String date;
final String description;
final bool has360View;
final SampleProject project;
@override
Widget build(BuildContext context) {
@@ -239,7 +261,7 @@ class _LibraryCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 20),
child: InkWell(
onTap: () {
context.push('/model-houses/$modelId');
context.push('/model-houses/${project.id}');
},
borderRadius: BorderRadius.circular(12),
child: Column(
@@ -252,8 +274,9 @@ class _LibraryCard extends StatelessWidget {
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: CachedNetworkImage(
imageUrl: imageUrl,
child: project.thumbnailUrl != null
? CachedNetworkImage(
imageUrl: project.thumbnailUrl!,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
@@ -271,9 +294,18 @@ class _LibraryCard extends StatelessWidget {
color: AppColors.grey500,
),
),
)
: Container(
height: 200,
color: AppColors.grey100,
child: const Icon(
Icons.home_work,
size: 48,
color: AppColors.grey500,
),
),
if (has360View)
),
if (project.has360View)
Positioned(
top: 12,
right: 12,
@@ -307,7 +339,7 @@ class _LibraryCard extends StatelessWidget {
children: [
// Title
Text(
title,
project.projectName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
@@ -315,39 +347,21 @@ class _LibraryCard extends StatelessWidget {
),
),
const SizedBox(height: 8),
// Date
Row(
children: [
const Icon(
Icons.calendar_today,
size: 14,
color: AppColors.grey500,
),
const SizedBox(width: 6),
Text(
'Ngày đăng: $date',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
if (project.plainDescription.isNotEmpty) ...[
const SizedBox(height: 12),
// Description
Text(
description,
project.plainDescription,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
height: 1.5,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
@@ -358,90 +372,118 @@ class _LibraryCard extends StatelessWidget {
}
/// Design Requests Tab
class _DesignRequestsTab extends StatelessWidget {
class _DesignRequestsTab extends ConsumerWidget {
const _DesignRequestsTab();
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(20),
children: const [
_RequestCard(
code: '#YC001',
status: DesignRequestStatus.completed,
date: '20/10/2024',
description: 'Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)',
Widget build(BuildContext context, WidgetRef ref) {
final requestsAsync = ref.watch(designRequestsListProvider);
return requestsAsync.when(
data: (requests) {
if (requests.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.design_services_outlined,
size: 64,
color: AppColors.grey500,
),
_RequestCard(
code: '#YC002',
status: DesignRequestStatus.designing,
date: '25/10/2024',
description: 'Cải tạo căn hộ chung cư - Chị Lan (Quận 2)',
SizedBox(height: 16),
Text(
'Chưa có yêu cầu thiết kế nào',
style: TextStyle(
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
class _RequestCard extends StatelessWidget {
const _RequestCard({
required this.code,
required this.status,
required this.date,
required this.description,
});
const _RequestCard({required this.request});
final String code;
final DesignRequestStatus status;
final String date;
final String description;
final DesignRequest request;
Color _getStatusColor() {
switch (status) {
switch (request.status) {
case DesignRequestStatus.pending:
return const Color(0xFFffc107); // Warning yellow
case DesignRequestStatus.designing:
return const Color(0xFF3730a3); // Indigo
case DesignRequestStatus.completed:
return const Color(0xFF065f46); // Success green
case DesignRequestStatus.rejected:
return const Color(0xFFdc2626); // Danger red
}
}
Color _getStatusBackgroundColor() {
switch (status) {
switch (request.status) {
case DesignRequestStatus.pending:
return const Color(0xFFfef3c7); // Light yellow
case DesignRequestStatus.designing:
return const Color(0xFFe0e7ff); // Light indigo
case DesignRequestStatus.completed:
return const Color(0xFFd1fae5); // Light green
}
}
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';
case DesignRequestStatus.rejected:
return const Color(0xFFfee2e2); // Light red
}
}
@@ -453,9 +495,7 @@ class _RequestCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () {
context.push(
'/model-houses/design-request/${code.replaceAll('#', '')}',
);
context.push('/model-houses/design-request/${request.id}');
},
borderRadius: BorderRadius.circular(12),
child: Padding(
@@ -467,14 +507,18 @@ class _RequestCard extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Mã yêu cầu: $code',
Expanded(
child: Text(
'Mã yêu cầu: #${request.id}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
@@ -485,7 +529,7 @@ class _RequestCard extends StatelessWidget {
borderRadius: BorderRadius.circular(20),
),
child: Text(
_getStatusText(),
request.statusText.toUpperCase(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
@@ -499,19 +543,35 @@ class _RequestCard extends StatelessWidget {
const SizedBox(height: 8),
// Date
if (request.dateline != null)
Text(
'Ngày gửi: $date',
'Deadline: ${request.dateline}',
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
),
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
Text(
description,
style: const TextStyle(fontSize: 14, color: AppColors.grey900),
request.plainDescription,
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';
}

View File

@@ -0,0 +1,58 @@
/// Providers: Sample Project
///
/// Riverpod providers for managing sample/model house projects state.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/features/showrooms/data/datasources/sample_project_remote_datasource.dart';
import 'package:worker/features/showrooms/data/repositories/sample_project_repository_impl.dart';
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
import 'package:worker/features/showrooms/domain/repositories/sample_project_repository.dart';
part 'sample_project_provider.g.dart';
/// Sample Project Remote Data Source Provider
@riverpod
Future<SampleProjectRemoteDataSource> sampleProjectRemoteDataSource(Ref ref) async {
final dioClient = await ref.watch(dioClientProvider.future);
return SampleProjectRemoteDataSourceImpl(dioClient);
}
/// Sample Project Repository Provider
@riverpod
Future<SampleProjectRepository> sampleProjectRepository(Ref ref) async {
final remoteDataSource = await ref.watch(sampleProjectRemoteDataSourceProvider.future);
return SampleProjectRepositoryImpl(remoteDataSource);
}
/// Sample Projects List Provider
///
/// Fetches and manages sample/model house projects from API.
@riverpod
class SampleProjectsList extends _$SampleProjectsList {
@override
Future<List<SampleProject>> build() async {
final repository = await ref.watch(sampleProjectRepositoryProvider.future);
return repository.getSampleProjects();
}
/// Refresh sample projects from remote
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = await ref.read(sampleProjectRepositoryProvider.future);
return repository.getSampleProjects();
});
}
}
/// Sample Project Detail Provider
///
/// Fetches detail of a specific sample project by name.
/// Uses family modifier to cache by project name.
@riverpod
Future<SampleProject> sampleProjectDetail(Ref ref, String name) async {
final repository = await ref.watch(sampleProjectRepositoryProvider.future);
return repository.getSampleProjectDetail(name);
}

View File

@@ -0,0 +1,266 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sample_project_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Sample Project Remote Data Source Provider
@ProviderFor(sampleProjectRemoteDataSource)
const sampleProjectRemoteDataSourceProvider =
SampleProjectRemoteDataSourceProvider._();
/// Sample Project Remote Data Source Provider
final class SampleProjectRemoteDataSourceProvider
extends
$FunctionalProvider<
AsyncValue<SampleProjectRemoteDataSource>,
SampleProjectRemoteDataSource,
FutureOr<SampleProjectRemoteDataSource>
>
with
$FutureModifier<SampleProjectRemoteDataSource>,
$FutureProvider<SampleProjectRemoteDataSource> {
/// Sample Project Remote Data Source Provider
const SampleProjectRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'sampleProjectRemoteDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$sampleProjectRemoteDataSourceHash();
@$internal
@override
$FutureProviderElement<SampleProjectRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SampleProjectRemoteDataSource> create(Ref ref) {
return sampleProjectRemoteDataSource(ref);
}
}
String _$sampleProjectRemoteDataSourceHash() =>
r'551677016d2d5d5185537f4871b161456370158e';
/// Sample Project Repository Provider
@ProviderFor(sampleProjectRepository)
const sampleProjectRepositoryProvider = SampleProjectRepositoryProvider._();
/// Sample Project Repository Provider
final class SampleProjectRepositoryProvider
extends
$FunctionalProvider<
AsyncValue<SampleProjectRepository>,
SampleProjectRepository,
FutureOr<SampleProjectRepository>
>
with
$FutureModifier<SampleProjectRepository>,
$FutureProvider<SampleProjectRepository> {
/// Sample Project Repository Provider
const SampleProjectRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'sampleProjectRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$sampleProjectRepositoryHash();
@$internal
@override
$FutureProviderElement<SampleProjectRepository> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SampleProjectRepository> create(Ref ref) {
return sampleProjectRepository(ref);
}
}
String _$sampleProjectRepositoryHash() =>
r'a675cd70c32a2d9331992a1db3d5646a38e0c953';
/// Sample Projects List Provider
///
/// Fetches and manages sample/model house projects from API.
@ProviderFor(SampleProjectsList)
const sampleProjectsListProvider = SampleProjectsListProvider._();
/// Sample Projects List Provider
///
/// Fetches and manages sample/model house projects from API.
final class SampleProjectsListProvider
extends $AsyncNotifierProvider<SampleProjectsList, List<SampleProject>> {
/// Sample Projects List Provider
///
/// Fetches and manages sample/model house projects from API.
const SampleProjectsListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'sampleProjectsListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$sampleProjectsListHash();
@$internal
@override
SampleProjectsList create() => SampleProjectsList();
}
String _$sampleProjectsListHash() =>
r'1255814621d429e09c8e0cdec38fbc91bacc1c77';
/// Sample Projects List Provider
///
/// Fetches and manages sample/model house projects from API.
abstract class _$SampleProjectsList
extends $AsyncNotifier<List<SampleProject>> {
FutureOr<List<SampleProject>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref as $Ref<AsyncValue<List<SampleProject>>, List<SampleProject>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<SampleProject>>, List<SampleProject>>,
AsyncValue<List<SampleProject>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Sample Project Detail Provider
///
/// Fetches detail of a specific sample project by name.
/// Uses family modifier to cache by project name.
@ProviderFor(sampleProjectDetail)
const sampleProjectDetailProvider = SampleProjectDetailFamily._();
/// Sample Project Detail Provider
///
/// Fetches detail of a specific sample project by name.
/// Uses family modifier to cache by project name.
final class SampleProjectDetailProvider
extends
$FunctionalProvider<
AsyncValue<SampleProject>,
SampleProject,
FutureOr<SampleProject>
>
with $FutureModifier<SampleProject>, $FutureProvider<SampleProject> {
/// Sample Project Detail Provider
///
/// Fetches detail of a specific sample project by name.
/// Uses family modifier to cache by project name.
const SampleProjectDetailProvider._({
required SampleProjectDetailFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'sampleProjectDetailProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$sampleProjectDetailHash();
@override
String toString() {
return r'sampleProjectDetailProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<SampleProject> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SampleProject> create(Ref ref) {
final argument = this.argument as String;
return sampleProjectDetail(ref, argument);
}
@override
bool operator ==(Object other) {
return other is SampleProjectDetailProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$sampleProjectDetailHash() =>
r'6ea1bf329f69e0274df7f072ef1494ed04c3238d';
/// Sample Project Detail Provider
///
/// Fetches detail of a specific sample project by name.
/// Uses family modifier to cache by project name.
final class SampleProjectDetailFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<SampleProject>, String> {
const SampleProjectDetailFamily._()
: super(
retry: null,
name: r'sampleProjectDetailProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Sample Project Detail Provider
///
/// Fetches detail of a specific sample project by name.
/// Uses family modifier to cache by project name.
SampleProjectDetailProvider call(String name) =>
SampleProjectDetailProvider._(argument: name, from: this);
@override
String toString() => r'sampleProjectDetailProvider';
}