Compare commits
4 Commits
6e7e848ad6
...
9e7bda32f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e7bda32f2 | ||
|
|
65f6f825a6 | ||
|
|
440b474504 | ||
|
|
ed6cc4cebc |
@@ -139,6 +139,14 @@ curl --location 'https://land.dbiz.com//api/method/upload_file' \
|
|||||||
--form 'docname="p9ti8veq2g"' \
|
--form 'docname="p9ti8veq2g"' \
|
||||||
--form 'optimize="true"'
|
--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
|
#get detail of a project
|
||||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_detail' \
|
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_detail' \
|
||||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
--header '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
73
docs/request.sh
Normal 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
61
docs/sample_project.sh
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -315,6 +315,47 @@ class ApiConstants {
|
|||||||
static const String getProjectDetail =
|
static const String getProjectDetail =
|
||||||
'/building_material.building_material.api.project.get_detail';
|
'/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)
|
/// Create new project (legacy endpoint - may be deprecated)
|
||||||
/// POST /projects
|
/// POST /projects
|
||||||
static const String createProject = '/projects';
|
static const String createProject = '/projects';
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ abstract class SubmissionsRemoteDataSource {
|
|||||||
required String projectName,
|
required String projectName,
|
||||||
required String filePath,
|
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
|
/// Submissions Remote Data Source Implementation
|
||||||
@@ -303,4 +311,41 @@ class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource {
|
|||||||
throw Exception('Failed to upload project file: $e');
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,4 +171,19 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteProjectFile({
|
||||||
|
required String fileId,
|
||||||
|
required String projectName,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await _remoteDataSource.deleteProjectFile(
|
||||||
|
fileId: fileId,
|
||||||
|
projectName: projectName,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,4 +54,12 @@ abstract class SubmissionsRepository {
|
|||||||
required String projectName,
|
required String projectName,
|
||||||
required String filePath,
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
List<ProjectFile> _existingFiles = []; // Existing files from API
|
List<ProjectFile> _existingFiles = []; // Existing files from API
|
||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
bool _isLoadingDetail = false;
|
bool _isLoadingDetail = false;
|
||||||
|
String? _deletingFileId; // Track which file is being deleted
|
||||||
|
|
||||||
/// Whether we're editing an existing submission
|
/// Whether we're editing an existing submission
|
||||||
bool get isEditing => widget.submission != null;
|
bool get isEditing => widget.submission != null;
|
||||||
@@ -858,17 +859,22 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
|
|
||||||
Widget _buildExistingFileItem(ProjectFile file, int index) {
|
Widget _buildExistingFileItem(ProjectFile file, int index) {
|
||||||
final fileName = file.fileUrl.split('/').last;
|
final fileName = file.fileUrl.split('/').last;
|
||||||
|
final isDeleting = _deletingFileId == file.id;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8F9FA),
|
color: const Color(0xFFF8F9FA),
|
||||||
border: Border.all(color: AppColors.success),
|
border: Border.all(
|
||||||
|
color: isDeleting ? AppColors.grey500 : AppColors.success,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Network image
|
// Network image with delete overlay
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: CachedNetworkImage(
|
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),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -909,25 +937,36 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
fileName,
|
fileName,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: isDeleting ? AppColors.grey500 : AppColors.grey900,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
const Text(
|
Text(
|
||||||
'Đã tải lên',
|
isDeleting ? 'Đang xóa...' : 'Đã tải lên',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
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(
|
const FaIcon(
|
||||||
FontAwesomeIcons.circleCheck,
|
FontAwesomeIcons.circleCheck,
|
||||||
size: 16,
|
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() {
|
Widget _buildSubmitButton() {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
lib/features/showrooms/data/models/design_request_model.dart
Normal file
103
lib/features/showrooms/data/models/design_request_model.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
136
lib/features/showrooms/data/models/sample_project_model.dart
Normal file
136
lib/features/showrooms/data/models/sample_project_model.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
lib/features/showrooms/domain/entities/design_request.dart
Normal file
118
lib/features/showrooms/domain/entities/design_request.dart
Normal 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(' ', ' ')
|
||||||
|
.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})';
|
||||||
|
}
|
||||||
|
}
|
||||||
95
lib/features/showrooms/domain/entities/sample_project.dart
Normal file
95
lib/features/showrooms/domain/entities/sample_project.dart
Normal 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(' ', ' ')
|
||||||
|
.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})';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,8 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
|
||||||
|
import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart';
|
||||||
|
|
||||||
/// Model House Detail Page
|
/// Model House Detail Page
|
||||||
class ModelHouseDetailPage extends ConsumerWidget {
|
class ModelHouseDetailPage extends ConsumerWidget {
|
||||||
@@ -24,8 +26,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// Mock data - in real app, fetch from provider
|
final detailAsync = ref.watch(sampleProjectDetailProvider(modelId));
|
||||||
final modelData = _getMockData(modelId);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: const Color(0xFFF4F6F8),
|
||||||
@@ -43,13 +44,16 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
style: TextStyle(color: Colors.black),
|
style: TextStyle(color: Colors.black),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
detailAsync.maybeWhen(
|
||||||
|
data: (project) => IconButton(
|
||||||
icon: const FaIcon(
|
icon: const FaIcon(
|
||||||
FontAwesomeIcons.shareNodes,
|
FontAwesomeIcons.shareNodes,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () => _shareModel(context, modelData),
|
onPressed: () => _shareModel(context, project),
|
||||||
|
),
|
||||||
|
orElse: () => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
const SizedBox(width: AppSpacing.sm),
|
const SizedBox(width: AppSpacing.sm),
|
||||||
],
|
],
|
||||||
@@ -57,34 +61,69 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
backgroundColor: AppColors.white,
|
backgroundColor: AppColors.white,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: detailAsync.when(
|
||||||
|
data: (project) => SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 360° View Launcher
|
// 360° View Launcher
|
||||||
_build360ViewLauncher(context, modelData),
|
_build360ViewLauncher(context, project),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Project Information
|
// Project Information
|
||||||
_buildProjectInfo(modelData),
|
_buildProjectInfo(project),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Image Gallery
|
// Image Gallery
|
||||||
_buildImageGallery(context, modelData),
|
if (project.filesList.isNotEmpty)
|
||||||
|
_buildImageGallery(context, project),
|
||||||
|
|
||||||
const SizedBox(height: 40),
|
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(
|
Widget _build360ViewLauncher(BuildContext context, SampleProject project) {
|
||||||
BuildContext context,
|
final hasImages = project.filesList.isNotEmpty;
|
||||||
Map<String, dynamic> modelData,
|
final firstImageUrl = hasImages ? project.filesList.first.fileUrl : project.thumbnailUrl;
|
||||||
) {
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.all(4),
|
margin: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -100,7 +139,9 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _launch360View(context, modelData['url360'] as String),
|
onTap: project.has360View
|
||||||
|
? () => _launch360View(context, project.viewUrl!)
|
||||||
|
: null,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 400,
|
height: 400,
|
||||||
@@ -115,15 +156,16 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Background image with overlay
|
// Background image with overlay
|
||||||
|
if (firstImageUrl != null)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: 0.3,
|
opacity: 0.3,
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: (modelData['images'] as List<Map<String, String>>)
|
imageUrl: firstImageUrl,
|
||||||
.first['url']!,
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
errorWidget: (context, url, error) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -190,6 +232,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
// Launch Button
|
// Launch Button
|
||||||
|
if (project.has360View)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 32,
|
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(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
@@ -257,45 +319,18 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Title
|
// Title
|
||||||
Text(
|
Text(
|
||||||
modelData['title'] as String,
|
project.projectName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: AppColors.grey900,
|
color: AppColors.grey900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (project.plainDescription.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
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
|
// Description
|
||||||
Text(
|
Text(
|
||||||
modelData['description'] as String,
|
project.plainDescription,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF4b5563),
|
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(
|
Widget _buildImageGallery(BuildContext context, SampleProject project) {
|
||||||
BuildContext context,
|
final images = project.filesList;
|
||||||
Map<String, dynamic> modelData,
|
|
||||||
) {
|
|
||||||
final images = modelData['images'] as List<Map<String, String>>;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
@@ -364,17 +364,17 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Gallery Title
|
// Gallery Title
|
||||||
const Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
FaIcon(
|
const FaIcon(
|
||||||
FontAwesomeIcons.images,
|
FontAwesomeIcons.images,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: AppColors.grey900,
|
color: AppColors.grey900,
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Thư viện Hình ảnh',
|
'Thư viện Hình ảnh (${images.length})',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: AppColors.grey900,
|
color: AppColors.grey900,
|
||||||
@@ -402,7 +402,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
width: 120,
|
width: 120,
|
||||||
height: 120,
|
height: 120,
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: image['url']!,
|
imageUrl: image.fileUrl,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (context, url) => Container(
|
placeholder: (context, url) => Container(
|
||||||
color: AppColors.grey100,
|
color: AppColors.grey100,
|
||||||
@@ -442,7 +442,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
|
|
||||||
void _showImageViewer(
|
void _showImageViewer(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<Map<String, String>> images,
|
List<ProjectFile> images,
|
||||||
int initialIndex,
|
int initialIndex,
|
||||||
) {
|
) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
@@ -455,62 +455,17 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _shareModel(BuildContext context, Map<String, dynamic> modelData) {
|
void _shareModel(BuildContext context, SampleProject project) {
|
||||||
Share.share(
|
final shareText = project.has360View
|
||||||
'Xem mô hình 360° ${modelData['title']}\n${modelData['url360']}',
|
? 'Xem mô hình 360° ${project.projectName}\n${project.viewUrl}'
|
||||||
subject: modelData['title'] as String,
|
: 'Xem nhà mẫu ${project.projectName}';
|
||||||
);
|
SharePlus.instance.share(ShareParams(text: shareText));
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Image Viewer Dialog with Swipe Navigation
|
/// Image Viewer Dialog with Swipe Navigation
|
||||||
class _ImageViewerDialog extends StatefulWidget {
|
class _ImageViewerDialog extends StatefulWidget {
|
||||||
final List<Map<String, String>> images;
|
final List<ProjectFile> images;
|
||||||
final int initialIndex;
|
final int initialIndex;
|
||||||
|
|
||||||
const _ImageViewerDialog({
|
const _ImageViewerDialog({
|
||||||
@@ -561,7 +516,7 @@ class _ImageViewerDialogState extends State<_ImageViewerDialog> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return Center(
|
return Center(
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: widget.images[index]['url']!,
|
imageUrl: widget.images[index].fileUrl,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
placeholder: (context, url) => const Center(
|
placeholder: (context, url) => const Center(
|
||||||
child: CircularProgressIndicator(color: Colors.white),
|
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ 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/presentation/providers/design_request_provider.dart';
|
||||||
|
import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart';
|
||||||
|
|
||||||
/// Model Houses Page
|
/// Model Houses Page
|
||||||
///
|
///
|
||||||
@@ -160,76 +164,94 @@ class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Library Tab - Model house 360° library
|
/// Library Tab - Model house 360° library
|
||||||
class _LibraryTab extends StatelessWidget {
|
class _LibraryTab extends ConsumerWidget {
|
||||||
const _LibraryTab();
|
const _LibraryTab();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ListView(
|
final sampleProjectsAsync = ref.watch(sampleProjectsListProvider);
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
children: const [
|
return sampleProjectsAsync.when(
|
||||||
_LibraryCard(
|
data: (projects) {
|
||||||
modelId: 'studio-01',
|
if (projects.isEmpty) {
|
||||||
imageUrl:
|
return const Center(
|
||||||
'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=200&fit=crop',
|
child: Padding(
|
||||||
title: 'Căn hộ Studio',
|
padding: EdgeInsets.all(40),
|
||||||
date: '15/11/2024',
|
child: Column(
|
||||||
description:
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
'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.',
|
children: [
|
||||||
has360View: true,
|
Icon(
|
||||||
|
Icons.home_work_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: AppColors.grey500,
|
||||||
),
|
),
|
||||||
_LibraryCard(
|
SizedBox(height: 16),
|
||||||
modelId: 'villa-01',
|
Text(
|
||||||
imageUrl:
|
'Chưa có mẫu nhà nào',
|
||||||
'https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=200&fit=crop',
|
style: TextStyle(
|
||||||
title: 'Biệt thự Hiện đại',
|
fontSize: 16,
|
||||||
date: '12/11/2024',
|
color: AppColors.grey500,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
_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
|
/// Library Card Widget
|
||||||
class _LibraryCard extends StatelessWidget {
|
class _LibraryCard extends StatelessWidget {
|
||||||
const _LibraryCard({
|
const _LibraryCard({required this.project});
|
||||||
required this.modelId,
|
|
||||||
required this.imageUrl,
|
|
||||||
required this.title,
|
|
||||||
required this.date,
|
|
||||||
required this.description,
|
|
||||||
this.has360View = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String modelId;
|
final SampleProject project;
|
||||||
final String imageUrl;
|
|
||||||
final String title;
|
|
||||||
final String date;
|
|
||||||
final String description;
|
|
||||||
final bool has360View;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -239,7 +261,7 @@ class _LibraryCard extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(bottom: 20),
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.push('/model-houses/$modelId');
|
context.push('/model-houses/${project.id}');
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -252,8 +274,9 @@ class _LibraryCard extends StatelessWidget {
|
|||||||
borderRadius: const BorderRadius.vertical(
|
borderRadius: const BorderRadius.vertical(
|
||||||
top: Radius.circular(12),
|
top: Radius.circular(12),
|
||||||
),
|
),
|
||||||
child: CachedNetworkImage(
|
child: project.thumbnailUrl != null
|
||||||
imageUrl: imageUrl,
|
? CachedNetworkImage(
|
||||||
|
imageUrl: project.thumbnailUrl!,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 200,
|
height: 200,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@@ -271,9 +294,18 @@ class _LibraryCard extends StatelessWidget {
|
|||||||
color: AppColors.grey500,
|
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(
|
Positioned(
|
||||||
top: 12,
|
top: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
@@ -307,7 +339,7 @@ class _LibraryCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Title
|
// Title
|
||||||
Text(
|
Text(
|
||||||
title,
|
project.projectName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
@@ -315,39 +347,21 @@ class _LibraryCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
if (project.plainDescription.isNotEmpty) ...[
|
||||||
|
|
||||||
// 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Description
|
// Description
|
||||||
Text(
|
Text(
|
||||||
description,
|
project.plainDescription,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.grey500,
|
color: AppColors.grey500,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -358,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';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,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(
|
||||||
@@ -467,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,
|
||||||
@@ -485,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,
|
||||||
@@ -499,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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user