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 'optimize="true"'
|
||||
|
||||
#delete image file of project
|
||||
curl --location 'https://land.dbiz.com//api/method/frappe.desk.form.utils.remove_attach' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--form 'fid="67803d2e95"' \ #file id to be deleted
|
||||
--form 'dt="Architectural Project"' \ #doctye
|
||||
--form 'dn="p9ti8veq2g"' #docname
|
||||
|
||||
#get detail of a project
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_detail' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
|
||||
73
docs/request.sh
Normal file
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 class="library-content" onclick="viewLibraryDetail('studio-apartment')">
|
||||
<h3 class="library-title">Căn hộ Studio</h3>
|
||||
<div class="library-date">
|
||||
<!--<div class="library-date">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Ngày đăng: 15/11/2024</span>
|
||||
</div>
|
||||
<p class="library-description">
|
||||
Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa.
|
||||
</p>
|
||||
</p>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -336,13 +336,13 @@
|
||||
</div>
|
||||
<div class="library-content">
|
||||
<h3 class="library-title">Biệt thự Hiện đại</h3>
|
||||
<div class="library-date">
|
||||
<!--<div class="library-date">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Ngày đăng: 12/11/2024</span>
|
||||
</div>
|
||||
<p class="library-description">
|
||||
Biệt thự 3 tầng với phong cách kiến trúc hiện đại, sử dụng gạch granite và ceramic premium tạo điểm nhấn.
|
||||
</p>
|
||||
</p>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -356,13 +356,13 @@
|
||||
</div>
|
||||
<div class="library-content">
|
||||
<h3 class="library-title">Nhà phố Tối giản</h3>
|
||||
<div class="library-date">
|
||||
<!--<div class="library-date">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Ngày đăng: 08/11/2024</span>
|
||||
</div>
|
||||
<p class="library-description">
|
||||
Nhà phố 4x15m với thiết kế tối giản, tận dụng ánh sáng tự nhiên và gạch men màu trung tính.
|
||||
</p>
|
||||
</p>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -376,13 +376,13 @@
|
||||
</div>
|
||||
<div class="library-content">
|
||||
<h3 class="library-title">Chung cư Cao cấp</h3>
|
||||
<div class="library-date">
|
||||
<!--<div class="library-date">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Ngày đăng: 05/11/2024</span>
|
||||
</div>
|
||||
<p class="library-description">
|
||||
Căn hộ 3PN với nội thất sang trọng, sử dụng gạch marble và ceramic cao cấp nhập khẩu Italy.
|
||||
</p>
|
||||
</p>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -315,6 +315,47 @@ class ApiConstants {
|
||||
static const String getProjectDetail =
|
||||
'/building_material.building_material.api.project.get_detail';
|
||||
|
||||
/// Delete project file/attachment (requires sid and csrf_token)
|
||||
/// POST /api/method/frappe.desk.form.utils.remove_attach
|
||||
/// Form-data: { "fid": "file_id", "dt": "Architectural Project", "dn": "project_name" }
|
||||
static const String removeProjectFile = '/frappe.desk.form.utils.remove_attach';
|
||||
|
||||
// ============================================================================
|
||||
// Sample Project / Model House Endpoints (Frappe ERPNext)
|
||||
// ============================================================================
|
||||
|
||||
/// Get list of sample/model house projects (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sample_project.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
/// Returns: { "message": [{ "name": "...", "project_name": "...", "notes": "...", "link": "...", "thumbnail": "..." }] }
|
||||
static const String getSampleProjectList =
|
||||
'/building_material.building_material.api.sample_project.get_list';
|
||||
|
||||
/// Get detail of a sample/model house project (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sample_project.get_detail
|
||||
/// Body: { "name": "PROJ-0001" }
|
||||
/// Returns: { "message": { "name": "...", "project_name": "...", "notes": "...", "link": "...", "thumbnail": "...", "files_list": [...] } }
|
||||
static const String getSampleProjectDetail =
|
||||
'/building_material.building_material.api.sample_project.get_detail';
|
||||
|
||||
// ============================================================================
|
||||
// Design Request Endpoints (Frappe ERPNext)
|
||||
// ============================================================================
|
||||
|
||||
/// Get list of design requests (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.design_request.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
/// Returns: { "message": [{ "name": "...", "subject": "...", "description": "...", "dateline": "...", "status": "...", "status_color": "..." }] }
|
||||
static const String getDesignRequestList =
|
||||
'/building_material.building_material.api.design_request.get_list';
|
||||
|
||||
/// Get detail of a design request (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.design_request.get_detail
|
||||
/// Body: { "name": "ISS-2025-00005" }
|
||||
/// Returns: { "message": { "name": "...", "subject": "...", "description": "...", "dateline": "...", "status": "...", "status_color": "...", "files_list": [...] } }
|
||||
static const String getDesignRequestDetail =
|
||||
'/building_material.building_material.api.design_request.get_detail';
|
||||
|
||||
/// Create new project (legacy endpoint - may be deprecated)
|
||||
/// POST /projects
|
||||
static const String createProject = '/projects';
|
||||
|
||||
@@ -43,6 +43,14 @@ abstract class SubmissionsRemoteDataSource {
|
||||
required String projectName,
|
||||
required String filePath,
|
||||
});
|
||||
|
||||
/// Delete a file from a project submission
|
||||
/// [fileId] is the file ID to delete
|
||||
/// [projectName] is the project name (docname)
|
||||
Future<void> deleteProjectFile({
|
||||
required String fileId,
|
||||
required String projectName,
|
||||
});
|
||||
}
|
||||
|
||||
/// Submissions Remote Data Source Implementation
|
||||
@@ -303,4 +311,41 @@ class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource {
|
||||
throw Exception('Failed to upload project file: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a file from a project submission
|
||||
///
|
||||
/// Calls: POST /api/method/frappe.desk.form.utils.remove_attach
|
||||
/// Form-data: fid, dt, dn
|
||||
@override
|
||||
Future<void> deleteProjectFile({
|
||||
required String fileId,
|
||||
required String projectName,
|
||||
}) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
'fid': fileId,
|
||||
'dt': 'Architectural Project',
|
||||
'dn': projectName,
|
||||
});
|
||||
|
||||
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||
'${ApiConstants.frappeApiMethod}${ApiConstants.removeProjectFile}',
|
||||
data: formData,
|
||||
);
|
||||
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw Exception('No data received from deleteProjectFile API');
|
||||
}
|
||||
|
||||
// Check for error in response
|
||||
if (data['exc_type'] != null || data['exception'] != null) {
|
||||
final errorMessage =
|
||||
data['_server_messages'] ?? data['exception'] ?? 'Unknown error';
|
||||
throw Exception('API error: $errorMessage');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to delete project file: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,4 +171,19 @@ class SubmissionsRepositoryImpl implements SubmissionsRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteProjectFile({
|
||||
required String fileId,
|
||||
required String projectName,
|
||||
}) async {
|
||||
try {
|
||||
return await _remoteDataSource.deleteProjectFile(
|
||||
fileId: fileId,
|
||||
projectName: projectName,
|
||||
);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,4 +54,12 @@ abstract class SubmissionsRepository {
|
||||
required String projectName,
|
||||
required String filePath,
|
||||
});
|
||||
|
||||
/// Delete a file from a project submission
|
||||
/// [fileId] is the file ID to delete
|
||||
/// [projectName] is the project name (docname)
|
||||
Future<void> deleteProjectFile({
|
||||
required String fileId,
|
||||
required String projectName,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
List<ProjectFile> _existingFiles = []; // Existing files from API
|
||||
bool _isSubmitting = false;
|
||||
bool _isLoadingDetail = false;
|
||||
String? _deletingFileId; // Track which file is being deleted
|
||||
|
||||
/// Whether we're editing an existing submission
|
||||
bool get isEditing => widget.submission != null;
|
||||
@@ -858,17 +859,22 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
|
||||
Widget _buildExistingFileItem(ProjectFile file, int index) {
|
||||
final fileName = file.fileUrl.split('/').last;
|
||||
final isDeleting = _deletingFileId == file.id;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
border: Border.all(color: AppColors.success),
|
||||
border: Border.all(
|
||||
color: isDeleting ? AppColors.grey500 : AppColors.success,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Network image
|
||||
// Network image with delete overlay
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CachedNetworkImage(
|
||||
@@ -902,6 +908,28 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Deleting overlay
|
||||
if (isDeleting)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -909,25 +937,36 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
children: [
|
||||
Text(
|
||||
fileName,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
color: isDeleting ? AppColors.grey500 : AppColors.grey900,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'Đã tải lên',
|
||||
Text(
|
||||
isDeleting ? 'Đang xóa...' : 'Đã tải lên',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.success,
|
||||
color: isDeleting ? AppColors.grey500 : AppColors.success,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Checkmark icon
|
||||
// Delete button or checkmark
|
||||
if (!_isSubmitting && !isDeleting)
|
||||
IconButton(
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.trash,
|
||||
size: 16,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
onPressed: () => _showDeleteConfirmDialog(file),
|
||||
tooltip: 'Xóa ảnh',
|
||||
)
|
||||
else if (!isDeleting)
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.circleCheck,
|
||||
size: 16,
|
||||
@@ -938,6 +977,147 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Show confirmation dialog before deleting an existing file
|
||||
Future<void> _showDeleteConfirmDialog(ProjectFile file) async {
|
||||
final fileName = file.fileUrl.split('/').last;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.triangleExclamation,
|
||||
color: AppColors.danger,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Text('Xác nhận xóa'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Bạn có chắc chắn muốn xóa ảnh này?'),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: file.fileUrl,
|
||||
width: 48,
|
||||
height: 48,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: AppColors.grey100,
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: AppColors.grey100,
|
||||
child: const Center(child: FaIcon(FontAwesomeIcons.image, size: 20)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
fileName,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Hành động này không thể hoàn tác.',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.danger,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Hủy'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.danger,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Xóa'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true && mounted) {
|
||||
await _deleteExistingFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete an existing file from the project
|
||||
Future<void> _deleteExistingFile(ProjectFile file) async {
|
||||
if (!isEditing || widget.submission == null) return;
|
||||
|
||||
setState(() => _deletingFileId = file.id);
|
||||
|
||||
try {
|
||||
final repository = await ref.read(submissionsRepositoryProvider.future);
|
||||
|
||||
await repository.deleteProjectFile(
|
||||
fileId: file.id,
|
||||
projectName: widget.submission!.submissionId,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Remove from local list
|
||||
setState(() {
|
||||
_existingFiles = _existingFiles.where((f) => f.id != file.id).toList();
|
||||
_deletingFileId = null;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Đã xóa ảnh thành công'),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _deletingFileId = null);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Lỗi xóa ảnh: ${e.toString().replaceAll('Exception: ', '')}'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSubmitButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
|
||||
@@ -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:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
|
||||
import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart';
|
||||
|
||||
/// Model House Detail Page
|
||||
class ModelHouseDetailPage extends ConsumerWidget {
|
||||
@@ -24,8 +26,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Mock data - in real app, fetch from provider
|
||||
final modelData = _getMockData(modelId);
|
||||
final detailAsync = ref.watch(sampleProjectDetailProvider(modelId));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
@@ -43,13 +44,16 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
style: TextStyle(color: Colors.black),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
detailAsync.maybeWhen(
|
||||
data: (project) => IconButton(
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.shareNodes,
|
||||
color: Colors.black,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => _shareModel(context, modelData),
|
||||
onPressed: () => _shareModel(context, project),
|
||||
),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
@@ -57,34 +61,69 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
backgroundColor: AppColors.white,
|
||||
centerTitle: false,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
body: detailAsync.when(
|
||||
data: (project) => SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 360° View Launcher
|
||||
_build360ViewLauncher(context, modelData),
|
||||
_build360ViewLauncher(context, project),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Project Information
|
||||
_buildProjectInfo(modelData),
|
||||
_buildProjectInfo(project),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Image Gallery
|
||||
_buildImageGallery(context, modelData),
|
||||
if (project.filesList.isNotEmpty)
|
||||
_buildImageGallery(context, project),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, stack) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.invalidate(sampleProjectDetailProvider(modelId)),
|
||||
child: const Text('Thử lại'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _build360ViewLauncher(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> modelData,
|
||||
) {
|
||||
Widget _build360ViewLauncher(BuildContext context, SampleProject project) {
|
||||
final hasImages = project.filesList.isNotEmpty;
|
||||
final firstImageUrl = hasImages ? project.filesList.first.fileUrl : project.thumbnailUrl;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -100,7 +139,9 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _launch360View(context, modelData['url360'] as String),
|
||||
onTap: project.has360View
|
||||
? () => _launch360View(context, project.viewUrl!)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 400,
|
||||
@@ -115,15 +156,16 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background image with overlay
|
||||
if (firstImageUrl != null)
|
||||
Positioned.fill(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Opacity(
|
||||
opacity: 0.3,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: (modelData['images'] as List<Map<String, String>>)
|
||||
.first['url']!,
|
||||
imageUrl: firstImageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -190,6 +232,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Launch Button
|
||||
if (project.has360View)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
@@ -225,6 +268,25 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: const Text(
|
||||
'Chưa có view 360°',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -237,7 +299,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProjectInfo(Map<String, dynamic> modelData) {
|
||||
Widget _buildProjectInfo(SampleProject project) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.all(20),
|
||||
@@ -257,45 +319,18 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
children: [
|
||||
// Title
|
||||
Text(
|
||||
modelData['title'] as String,
|
||||
project.projectName,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
if (project.plainDescription.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Specs Grid
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildSpecItem(
|
||||
'Diện tích',
|
||||
modelData['area'] as String,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildSpecItem(
|
||||
'Địa điểm',
|
||||
modelData['location'] as String,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildSpecItem(
|
||||
'Phong cách',
|
||||
modelData['style'] as String,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Description
|
||||
Text(
|
||||
modelData['description'] as String,
|
||||
project.plainDescription,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4b5563),
|
||||
@@ -303,48 +338,13 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpecItem(String label, String value) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageGallery(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> modelData,
|
||||
) {
|
||||
final images = modelData['images'] as List<Map<String, String>>;
|
||||
Widget _buildImageGallery(BuildContext context, SampleProject project) {
|
||||
final images = project.filesList;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
@@ -364,17 +364,17 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Gallery Title
|
||||
const Row(
|
||||
Row(
|
||||
children: [
|
||||
FaIcon(
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.images,
|
||||
size: 18,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Thư viện Hình ảnh',
|
||||
style: TextStyle(
|
||||
'Thư viện Hình ảnh (${images.length})',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
@@ -402,7 +402,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: image['url']!,
|
||||
imageUrl: image.fileUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
color: AppColors.grey100,
|
||||
@@ -442,7 +442,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
|
||||
void _showImageViewer(
|
||||
BuildContext context,
|
||||
List<Map<String, String>> images,
|
||||
List<ProjectFile> images,
|
||||
int initialIndex,
|
||||
) {
|
||||
showDialog<void>(
|
||||
@@ -455,62 +455,17 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _shareModel(BuildContext context, Map<String, dynamic> modelData) {
|
||||
Share.share(
|
||||
'Xem mô hình 360° ${modelData['title']}\n${modelData['url360']}',
|
||||
subject: modelData['title'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getMockData(String modelId) {
|
||||
// Mock data - in real app, fetch from repository
|
||||
return {
|
||||
'title': 'Căn hộ Studio',
|
||||
'area': '35m²',
|
||||
'location': 'Quận 7',
|
||||
'style': 'Hiện đại',
|
||||
'description':
|
||||
'Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.',
|
||||
'url360': 'https://vr.house3d.com/web/panorama-player/H00179549',
|
||||
'images': [
|
||||
{
|
||||
'url':
|
||||
'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=600&fit=crop',
|
||||
'caption': 'Phối cảnh tổng thể căn hộ studio với thiết kế hiện đại',
|
||||
},
|
||||
{
|
||||
'url':
|
||||
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/main_img.jpg',
|
||||
'caption': 'Khu vực phòng khách với gạch granite cao cấp',
|
||||
},
|
||||
{
|
||||
'url':
|
||||
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_1.jpg?v=1',
|
||||
'caption': 'Phòng ngủ chính với gạch ceramic màu trung tính',
|
||||
},
|
||||
{
|
||||
'url':
|
||||
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_0.jpg?v=1',
|
||||
'caption': 'Khu vực bếp với gạch mosaic điểm nhấn',
|
||||
},
|
||||
{
|
||||
'url':
|
||||
'https://images.unsplash.com/photo-1620626011761-996317b8d101?w=800&h=600&fit=crop',
|
||||
'caption': 'Phòng tắm hiện đại với gạch chống thấm cao cấp',
|
||||
},
|
||||
{
|
||||
'url':
|
||||
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_3.jpg?v=1',
|
||||
'caption': 'Khu vực bàn ăn ấm cúng',
|
||||
},
|
||||
],
|
||||
};
|
||||
void _shareModel(BuildContext context, SampleProject project) {
|
||||
final shareText = project.has360View
|
||||
? 'Xem mô hình 360° ${project.projectName}\n${project.viewUrl}'
|
||||
: 'Xem nhà mẫu ${project.projectName}';
|
||||
SharePlus.instance.share(ShareParams(text: shareText));
|
||||
}
|
||||
}
|
||||
|
||||
/// Image Viewer Dialog with Swipe Navigation
|
||||
class _ImageViewerDialog extends StatefulWidget {
|
||||
final List<Map<String, String>> images;
|
||||
final List<ProjectFile> images;
|
||||
final int initialIndex;
|
||||
|
||||
const _ImageViewerDialog({
|
||||
@@ -561,7 +516,7 @@ class _ImageViewerDialogState extends State<_ImageViewerDialog> {
|
||||
itemBuilder: (context, index) {
|
||||
return Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.images[index]['url']!,
|
||||
imageUrl: widget.images[index].fileUrl,
|
||||
fit: BoxFit.contain,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
@@ -618,34 +573,6 @@ class _ImageViewerDialogState extends State<_ImageViewerDialog> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Caption at bottom
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withValues(alpha: 0.7),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
widget.images[_currentIndex]['caption']!,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -10,6 +10,10 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/showrooms/domain/entities/design_request.dart';
|
||||
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
|
||||
import 'package:worker/features/showrooms/presentation/providers/design_request_provider.dart';
|
||||
import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart';
|
||||
|
||||
/// Model Houses Page
|
||||
///
|
||||
@@ -160,76 +164,94 @@ class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
|
||||
}
|
||||
|
||||
/// Library Tab - Model house 360° library
|
||||
class _LibraryTab extends StatelessWidget {
|
||||
class _LibraryTab extends ConsumerWidget {
|
||||
const _LibraryTab();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: const [
|
||||
_LibraryCard(
|
||||
modelId: 'studio-01',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=200&fit=crop',
|
||||
title: 'Căn hộ Studio',
|
||||
date: '15/11/2024',
|
||||
description:
|
||||
'Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa.',
|
||||
has360View: true,
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final sampleProjectsAsync = ref.watch(sampleProjectsListProvider);
|
||||
|
||||
return sampleProjectsAsync.when(
|
||||
data: (projects) {
|
||||
if (projects.isEmpty) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.home_work_outlined,
|
||||
size: 64,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
_LibraryCard(
|
||||
modelId: 'villa-01',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=200&fit=crop',
|
||||
title: 'Biệt thự Hiện đại',
|
||||
date: '12/11/2024',
|
||||
description:
|
||||
'Biệt thự 3 tầng với phong cách kiến trúc hiện đại, sử dụng gạch granite và ceramic premium tạo điểm nhấn.',
|
||||
has360View: true,
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Chưa có mẫu nhà nào',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
_LibraryCard(
|
||||
modelId: 'townhouse-01',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1562663474-6cbb3eaa4d14?w=800&h=200&fit=crop',
|
||||
title: 'Nhà phố Tối giản',
|
||||
date: '08/11/2024',
|
||||
description:
|
||||
'Nhà phố 4x15m với thiết kế tối giản, tận dụng ánh sáng tự nhiên và gạch men màu trung tính.',
|
||||
has360View: true,
|
||||
),
|
||||
_LibraryCard(
|
||||
modelId: 'apartment-01',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&h=200&fit=crop',
|
||||
title: 'Chung cư Cao cấp',
|
||||
date: '05/11/2024',
|
||||
description:
|
||||
'Căn hộ 3PN với nội thất sang trọng, sử dụng gạch marble và ceramic cao cấp nhập khẩu Italy.',
|
||||
has360View: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(sampleProjectsListProvider.notifier).refresh(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(20),
|
||||
itemCount: projects.length,
|
||||
itemBuilder: (context, index) {
|
||||
final project = projects[index];
|
||||
return _LibraryCard(project: project);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, stack) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.invalidate(sampleProjectsListProvider),
|
||||
child: const Text('Thử lại'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Library Card Widget
|
||||
class _LibraryCard extends StatelessWidget {
|
||||
const _LibraryCard({
|
||||
required this.modelId,
|
||||
required this.imageUrl,
|
||||
required this.title,
|
||||
required this.date,
|
||||
required this.description,
|
||||
this.has360View = false,
|
||||
});
|
||||
const _LibraryCard({required this.project});
|
||||
|
||||
final String modelId;
|
||||
final String imageUrl;
|
||||
final String title;
|
||||
final String date;
|
||||
final String description;
|
||||
final bool has360View;
|
||||
final SampleProject project;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -239,7 +261,7 @@ class _LibraryCard extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
context.push('/model-houses/$modelId');
|
||||
context.push('/model-houses/${project.id}');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Column(
|
||||
@@ -252,8 +274,9 @@ class _LibraryCard extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
child: project.thumbnailUrl != null
|
||||
? CachedNetworkImage(
|
||||
imageUrl: project.thumbnailUrl!,
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
fit: BoxFit.cover,
|
||||
@@ -271,9 +294,18 @@ class _LibraryCard extends StatelessWidget {
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
height: 200,
|
||||
color: AppColors.grey100,
|
||||
child: const Icon(
|
||||
Icons.home_work,
|
||||
size: 48,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
if (has360View)
|
||||
),
|
||||
if (project.has360View)
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
@@ -307,7 +339,7 @@ class _LibraryCard extends StatelessWidget {
|
||||
children: [
|
||||
// Title
|
||||
Text(
|
||||
title,
|
||||
project.projectName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -315,39 +347,21 @@ class _LibraryCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Date
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.calendar_today,
|
||||
size: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Ngày đăng: $date',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (project.plainDescription.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Description
|
||||
Text(
|
||||
description,
|
||||
project.plainDescription,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
height: 1.5,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -358,90 +372,118 @@ class _LibraryCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Design Requests Tab
|
||||
class _DesignRequestsTab extends StatelessWidget {
|
||||
class _DesignRequestsTab extends ConsumerWidget {
|
||||
const _DesignRequestsTab();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: const [
|
||||
_RequestCard(
|
||||
code: '#YC001',
|
||||
status: DesignRequestStatus.completed,
|
||||
date: '20/10/2024',
|
||||
description: 'Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)',
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final requestsAsync = ref.watch(designRequestsListProvider);
|
||||
|
||||
return requestsAsync.when(
|
||||
data: (requests) {
|
||||
if (requests.isEmpty) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.design_services_outlined,
|
||||
size: 64,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
_RequestCard(
|
||||
code: '#YC002',
|
||||
status: DesignRequestStatus.designing,
|
||||
date: '25/10/2024',
|
||||
description: 'Cải tạo căn hộ chung cư - Chị Lan (Quận 2)',
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Chưa có yêu cầu thiết kế nào',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
_RequestCard(
|
||||
code: '#YC003',
|
||||
status: DesignRequestStatus.pending,
|
||||
date: '28/10/2024',
|
||||
description: 'Thiết kế biệt thự 2 tầng - Anh Đức (Bình Dương)',
|
||||
),
|
||||
_RequestCard(
|
||||
code: '#YC004',
|
||||
status: DesignRequestStatus.pending,
|
||||
date: '01/11/2024',
|
||||
description: 'Thiết kế cửa hàng kinh doanh - Chị Mai (Quận 1)',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(designRequestsListProvider.notifier).refresh(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(20),
|
||||
itemCount: requests.length,
|
||||
itemBuilder: (context, index) {
|
||||
final request = requests[index];
|
||||
return _RequestCard(request: request);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, stack) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.invalidate(designRequestsListProvider),
|
||||
child: const Text('Thử lại'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Design Request Status
|
||||
enum DesignRequestStatus { pending, designing, completed }
|
||||
|
||||
/// Request Card Widget
|
||||
class _RequestCard extends StatelessWidget {
|
||||
const _RequestCard({
|
||||
required this.code,
|
||||
required this.status,
|
||||
required this.date,
|
||||
required this.description,
|
||||
});
|
||||
const _RequestCard({required this.request});
|
||||
|
||||
final String code;
|
||||
final DesignRequestStatus status;
|
||||
final String date;
|
||||
final String description;
|
||||
final DesignRequest request;
|
||||
|
||||
Color _getStatusColor() {
|
||||
switch (status) {
|
||||
switch (request.status) {
|
||||
case DesignRequestStatus.pending:
|
||||
return const Color(0xFFffc107); // Warning yellow
|
||||
case DesignRequestStatus.designing:
|
||||
return const Color(0xFF3730a3); // Indigo
|
||||
case DesignRequestStatus.completed:
|
||||
return const Color(0xFF065f46); // Success green
|
||||
case DesignRequestStatus.rejected:
|
||||
return const Color(0xFFdc2626); // Danger red
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusBackgroundColor() {
|
||||
switch (status) {
|
||||
switch (request.status) {
|
||||
case DesignRequestStatus.pending:
|
||||
return const Color(0xFFfef3c7); // Light yellow
|
||||
case DesignRequestStatus.designing:
|
||||
return const Color(0xFFe0e7ff); // Light indigo
|
||||
case DesignRequestStatus.completed:
|
||||
return const Color(0xFFd1fae5); // Light green
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatusText() {
|
||||
switch (status) {
|
||||
case DesignRequestStatus.pending:
|
||||
return 'CHỜ TIẾP NHẬN';
|
||||
case DesignRequestStatus.designing:
|
||||
return 'ĐANG THIẾT KẾ';
|
||||
case DesignRequestStatus.completed:
|
||||
return 'HOÀN THÀNH';
|
||||
case DesignRequestStatus.rejected:
|
||||
return const Color(0xFFfee2e2); // Light red
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,9 +495,7 @@ class _RequestCard extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
context.push(
|
||||
'/model-houses/design-request/${code.replaceAll('#', '')}',
|
||||
);
|
||||
context.push('/model-houses/design-request/${request.id}');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
@@ -467,14 +507,18 @@ class _RequestCard extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Mã yêu cầu: $code',
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Mã yêu cầu: #${request.id}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
@@ -485,7 +529,7 @@ class _RequestCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusText(),
|
||||
request.statusText.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -499,19 +543,35 @@ class _RequestCard extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Date
|
||||
if (request.dateline != null)
|
||||
Text(
|
||||
'Ngày gửi: $date',
|
||||
'Deadline: ${request.dateline}',
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Subject
|
||||
Text(
|
||||
request.subject,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
|
||||
if (request.plainDescription.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
// Description
|
||||
Text(
|
||||
description,
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.grey900),
|
||||
request.plainDescription,
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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