add auth, format

This commit is contained in:
Phuoc Nguyen
2025-11-07 11:52:06 +07:00
parent 24a8508fce
commit 3803bd26e0
173 changed files with 8505 additions and 7116 deletions

View File

@@ -46,7 +46,8 @@ class DesignRequestCreatePage extends HookConsumerWidget {
// Validate file sizes
final validFiles = <PlatformFile>[];
for (final file in result.files) {
if (file.size <= 10 * 1024 * 1024) { // 10MB max
if (file.size <= 10 * 1024 * 1024) {
// 10MB max
validFiles.add(file);
} else {
if (context.mounted) {
@@ -131,9 +132,7 @@ class DesignRequestCreatePage extends HookConsumerWidget {
fontWeight: FontWeight.w600,
),
),
actions: const [
SizedBox(width: AppSpacing.sm),
],
actions: const [SizedBox(width: AppSpacing.sm)],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
@@ -272,20 +271,31 @@ class DesignRequestCreatePage extends HookConsumerWidget {
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: selectedStyle.value.isEmpty ? null : selectedStyle.value,
value: selectedStyle.value.isEmpty
? null
: selectedStyle.value,
decoration: InputDecoration(
hintText: '-- Chọn phong cách --',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
borderSide: const BorderSide(
color: AppColors.grey100,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
borderSide: const BorderSide(
color: AppColors.grey100,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
@@ -293,14 +303,38 @@ class DesignRequestCreatePage extends HookConsumerWidget {
),
),
items: const [
DropdownMenuItem(value: 'hien-dai', child: Text('Hiện đại')),
DropdownMenuItem(value: 'toi-gian', child: Text('Tối giản')),
DropdownMenuItem(value: 'co-dien', child: Text('Cổ điển')),
DropdownMenuItem(value: 'scandinavian', child: Text('Scandinavian')),
DropdownMenuItem(value: 'industrial', child: Text('Industrial')),
DropdownMenuItem(value: 'tropical', child: Text('Tropical')),
DropdownMenuItem(value: 'luxury', child: Text('Luxury')),
DropdownMenuItem(value: 'khac', child: Text('Khác (ghi rõ trong ghi chú)')),
DropdownMenuItem(
value: 'hien-dai',
child: Text('Hiện đại'),
),
DropdownMenuItem(
value: 'toi-gian',
child: Text('Tối giản'),
),
DropdownMenuItem(
value: 'co-dien',
child: Text('Cổ điển'),
),
DropdownMenuItem(
value: 'scandinavian',
child: Text('Scandinavian'),
),
DropdownMenuItem(
value: 'industrial',
child: Text('Industrial'),
),
DropdownMenuItem(
value: 'tropical',
child: Text('Tropical'),
),
DropdownMenuItem(
value: 'luxury',
child: Text('Luxury'),
),
DropdownMenuItem(
value: 'khac',
child: Text('Khác (ghi rõ trong ghi chú)'),
),
],
onChanged: (value) {
selectedStyle.value = value ?? '';
@@ -331,20 +365,31 @@ class DesignRequestCreatePage extends HookConsumerWidget {
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: selectedBudget.value.isEmpty ? null : selectedBudget.value,
value: selectedBudget.value.isEmpty
? null
: selectedBudget.value,
decoration: InputDecoration(
hintText: '-- Chọn ngân sách --',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
borderSide: const BorderSide(
color: AppColors.grey100,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
borderSide: const BorderSide(
color: AppColors.grey100,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
@@ -352,12 +397,30 @@ class DesignRequestCreatePage extends HookConsumerWidget {
),
),
items: const [
DropdownMenuItem(value: 'duoi-100tr', child: Text('Dưới 100 triệu')),
DropdownMenuItem(value: '100-300tr', child: Text('100 - 300 triệu')),
DropdownMenuItem(value: '300-500tr', child: Text('300 - 500 triệu')),
DropdownMenuItem(value: '500tr-1ty', child: Text('500 triệu - 1 tỷ')),
DropdownMenuItem(value: 'tren-1ty', child: Text('Trên 1 tỷ')),
DropdownMenuItem(value: 'trao-doi', child: Text('Trao đổi trực tiếp')),
DropdownMenuItem(
value: 'duoi-100tr',
child: Text('Dưới 100 triệu'),
),
DropdownMenuItem(
value: '100-300tr',
child: Text('100 - 300 triệu'),
),
DropdownMenuItem(
value: '300-500tr',
child: Text('300 - 500 triệu'),
),
DropdownMenuItem(
value: '500tr-1ty',
child: Text('500 triệu - 1 tỷ'),
),
DropdownMenuItem(
value: 'tren-1ty',
child: Text('Trên 1 tỷ'),
),
DropdownMenuItem(
value: 'trao-doi',
child: Text('Trao đổi trực tiếp'),
),
],
onChanged: (value) {
selectedBudget.value = value ?? '';
@@ -429,18 +492,28 @@ class DesignRequestCreatePage extends HookConsumerWidget {
controller: notesController,
maxLines: 5,
decoration: InputDecoration(
hintText: 'Mô tả chi tiết về yêu cầu thiết kế, số phòng, công năng sử dụng, sở thích cá nhân...',
hintText:
'Mô tả chi tiết về yêu cầu thiết kế, số phòng, công năng sử dụng, sở thích cá nhân...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
borderSide: const BorderSide(
color: AppColors.grey100,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
borderSide: const BorderSide(
color: AppColors.grey100,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
width: 2,
),
),
contentPadding: const EdgeInsets.all(16),
),
@@ -626,8 +699,8 @@ class _ProgressStep extends StatelessWidget {
color: isCompleted
? AppColors.success
: isActive
? AppColors.primaryBlue
: AppColors.grey100,
? AppColors.primaryBlue
: AppColors.grey100,
),
child: Center(
child: Text(
@@ -635,7 +708,9 @@ class _ProgressStep extends StatelessWidget {
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isActive || isCompleted ? AppColors.white : AppColors.grey500,
color: isActive || isCompleted
? AppColors.white
: AppColors.grey500,
),
),
),
@@ -700,7 +775,10 @@ class _FormField extends StatelessWidget {
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
@@ -727,10 +805,7 @@ class _FilePreviewItem extends StatelessWidget {
final PlatformFile file;
final VoidCallback onRemove;
const _FilePreviewItem({
required this.file,
required this.onRemove,
});
const _FilePreviewItem({required this.file, required this.onRemove});
IconData _getFileIcon() {
final extension = file.extension?.toLowerCase();
@@ -765,11 +840,7 @@ class _FilePreviewItem extends StatelessWidget {
color: AppColors.primaryBlue,
borderRadius: BorderRadius.circular(6),
),
child: Icon(
_getFileIcon(),
color: AppColors.white,
size: 20,
),
child: Icon(_getFileIcon(), color: AppColors.white, size: 20),
),
const SizedBox(width: 12),
Expanded(
@@ -802,10 +873,7 @@ class _FilePreviewItem extends StatelessWidget {
color: AppColors.danger,
onPressed: onRemove,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
),
],
),

View File

@@ -23,10 +23,7 @@ import 'package:worker/features/showrooms/presentation/pages/model_houses_page.d
/// - Status timeline
/// - Action buttons (edit, contact)
class DesignRequestDetailPage extends ConsumerWidget {
const DesignRequestDetailPage({
required this.requestId,
super.key,
});
const DesignRequestDetailPage({required this.requestId, super.key});
final String requestId;
@@ -252,7 +249,11 @@ class DesignRequestDetailPage extends ConsumerWidget {
);
}
Future<void> _shareRequest(BuildContext context, String requestId, String name) async {
Future<void> _shareRequest(
BuildContext context,
String requestId,
String name,
) async {
try {
await Share.share(
'Yêu cầu thiết kế #$requestId\n$name',
@@ -402,9 +403,7 @@ class DesignRequestDetailPage extends ConsumerWidget {
const SizedBox(height: 12),
const Text(
'Thiết kế 3D của bạn đã sẵn sàng để xem',
style: TextStyle(
color: Color(0xFF065f46),
),
style: TextStyle(color: Color(0xFF065f46)),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
@@ -451,10 +450,7 @@ class DesignRequestDetailPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Project Name
_SectionHeader(
icon: Icons.info,
title: 'Thông tin dự án',
),
_SectionHeader(icon: Icons.info, title: 'Thông tin dự án'),
const SizedBox(height: 12),
RichText(
text: TextSpan(
@@ -476,10 +472,7 @@ class DesignRequestDetailPage extends ConsumerWidget {
const SizedBox(height: 24),
// Description
_SectionHeader(
icon: Icons.edit,
title: 'Mô tả yêu cầu',
),
_SectionHeader(icon: Icons.edit, title: 'Mô tả yêu cầu'),
const SizedBox(height: 12),
Text(
request['description'] as String,
@@ -525,10 +518,8 @@ class DesignRequestDetailPage extends ConsumerWidget {
)
else
...files.map(
(file) => _FileItem(
fileName: file,
icon: _getFileIcon(file),
),
(file) =>
_FileItem(fileName: file, icon: _getFileIcon(file)),
),
],
),
@@ -553,25 +544,22 @@ class DesignRequestDetailPage extends ConsumerWidget {
title: 'Lịch sử trạng thái',
),
const SizedBox(height: 16),
...List.generate(
timeline.length,
(index) {
final item = timeline[index];
return _TimelineItem(
title: item['title'] as String,
description: item['description'] as String,
date: item['date'] as String,
status: item['status'] as DesignRequestStatus,
icon: _getTimelineIcon(
item['status'] as DesignRequestStatus,
timeline.length - index - 1,
),
isLast: index == timeline.length - 1,
getStatusColor: _getStatusColor,
getStatusBackgroundColor: _getStatusBackgroundColor,
);
},
),
...List.generate(timeline.length, (index) {
final item = timeline[index];
return _TimelineItem(
title: item['title'] as String,
description: item['description'] as String,
date: item['date'] as String,
status: item['status'] as DesignRequestStatus,
icon: _getTimelineIcon(
item['status'] as DesignRequestStatus,
timeline.length - index - 1,
),
isLast: index == timeline.length - 1,
getStatusColor: _getStatusColor,
getStatusBackgroundColor: _getStatusBackgroundColor,
);
}),
],
),
),
@@ -587,7 +575,10 @@ class DesignRequestDetailPage extends ConsumerWidget {
onPressed: () => _editRequest(context),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.grey900,
side: const BorderSide(color: AppColors.grey100, width: 2),
side: const BorderSide(
color: AppColors.grey100,
width: 2,
),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
@@ -678,10 +669,7 @@ class _InfoGrid extends StatelessWidget {
/// Info Item Widget
class _InfoItem extends StatelessWidget {
const _InfoItem({
required this.label,
required this.value,
});
const _InfoItem({required this.label, required this.value});
final String label;
final String value;
@@ -723,10 +711,7 @@ class _InfoItem extends StatelessWidget {
/// Section Header Widget
class _SectionHeader extends StatelessWidget {
const _SectionHeader({
required this.icon,
required this.title,
});
const _SectionHeader({required this.icon, required this.title});
final IconData icon;
final String title;
@@ -752,10 +737,7 @@ class _SectionHeader extends StatelessWidget {
/// File Item Widget
class _FileItem extends StatelessWidget {
const _FileItem({
required this.fileName,
required this.icon,
});
const _FileItem({required this.fileName, required this.icon});
final String fileName;
final IconData icon;
@@ -835,11 +817,7 @@ class _TimelineItem extends StatelessWidget {
color: getStatusBackgroundColor(status),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: getStatusColor(status),
size: 20,
),
child: Icon(icon, color: getStatusColor(status), size: 20),
),
if (!isLast)
Expanded(

View File

@@ -54,13 +54,21 @@ class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
children: [
Text('Đây là nội dung hướng dẫn sử dụng cho tính năng Nhà mẫu:'),
SizedBox(height: 12),
Text('• Tab "Thư viện Mẫu 360": Là nơi công ty cung cấp các mẫu thiết kế 360° có sẵn để bạn tham khảo.'),
Text(
'• Tab "Thư viện Mẫu 360": Là nơi công ty cung cấp các mẫu thiết kế 360° có sẵn để bạn tham khảo.',
),
SizedBox(height: 8),
Text('• Tab "Yêu cầu Thiết kế": Là nơi bạn gửi yêu cầu (ticket) để đội ngũ thiết kế của chúng tôi hỗ trợ bạn.'),
Text(
'• Tab "Yêu cầu Thiết kế": Là nơi bạn gửi yêu cầu (ticket) để đội ngũ thiết kế của chúng tôi hỗ trợ bạn.',
),
SizedBox(height: 8),
Text('• Bấm nút "+" trong tab "Yêu cầu Thiết kế" để tạo một Yêu cầu Thiết kế mới.'),
Text(
'• Bấm nút "+" trong tab "Yêu cầu Thiết kế" để tạo một Yêu cầu Thiết kế mới.',
),
SizedBox(height: 8),
Text('• Khi yêu cầu hoàn thành, bạn có thể xem link thiết kế 3D trong trang chi tiết yêu cầu.'),
Text(
'• Khi yêu cầu hoàn thành, bạn có thể xem link thiết kế 3D trong trang chi tiết yêu cầu.',
),
],
),
),
@@ -127,10 +135,7 @@ class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
),
body: TabBarView(
controller: _tabController,
children: const [
_LibraryTab(),
_DesignRequestsTab(),
],
children: const [_LibraryTab(), _DesignRequestsTab()],
),
floatingActionButton: AnimatedBuilder(
animation: _tabController,
@@ -141,7 +146,11 @@ class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
onPressed: _createNewRequest,
backgroundColor: AppColors.primaryBlue,
elevation: 4,
child: const Icon(Icons.add, color: AppColors.white, size: 28),
child: const Icon(
Icons.add,
color: AppColors.white,
size: 28,
),
)
: const SizedBox.shrink();
},
@@ -160,31 +169,39 @@ class _LibraryTab extends StatelessWidget {
padding: const EdgeInsets.all(20),
children: const [
_LibraryCard(
imageUrl: 'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=200&fit=crop',
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.',
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,
),
_LibraryCard(
imageUrl: 'https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=200&fit=crop',
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.',
description:
'Biệt thự 3 tầng với phong cách kiến trúc hiện đại, sử dụng gạch granite và ceramic premium tạo điểm nhấn.',
has360View: true,
),
_LibraryCard(
imageUrl: 'https://images.unsplash.com/photo-1562663474-6cbb3eaa4d14?w=800&h=200&fit=crop',
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.',
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(
imageUrl: 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&h=200&fit=crop',
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.',
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,
),
],
@@ -212,15 +229,15 @@ class _LibraryCard extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.only(bottom: 20),
child: InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Chức năng xem chi tiết sẽ được triển khai trong phiên bản tiếp theo'),
content: Text(
'Chức năng xem chi tiết sẽ được triển khai trong phiên bản tiếp theo',
),
),
);
},
@@ -243,9 +260,7 @@ class _LibraryCard extends StatelessWidget {
placeholder: (context, url) => Container(
height: 200,
color: AppColors.grey100,
child: const Center(
child: CircularProgressIndicator(),
),
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => Container(
height: 200,
@@ -381,11 +396,7 @@ class _DesignRequestsTab extends StatelessWidget {
}
/// Design Request Status
enum DesignRequestStatus {
pending,
designing,
completed,
}
enum DesignRequestStatus { pending, designing, completed }
/// Request Card Widget
class _RequestCard extends StatelessWidget {
@@ -438,13 +449,13 @@ class _RequestCard extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () {
context.push('/model-houses/design-request/${code.replaceAll('#', '')}');
context.push(
'/model-houses/design-request/${code.replaceAll('#', '')}',
);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
@@ -490,10 +501,7 @@ class _RequestCard extends StatelessWidget {
// Date
Text(
'Ngày gửi: $date',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
),
const SizedBox(height: 8),
@@ -501,10 +509,7 @@ class _RequestCard extends StatelessWidget {
// Description
Text(
description,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey900,
),
style: const TextStyle(fontSize: 14, color: AppColors.grey900),
),
],
),