add model/design
This commit is contained in:
@@ -0,0 +1,814 @@
|
||||
/// Page: Design Request Create Page
|
||||
///
|
||||
/// Form to create a new design request following html/design-request-create.html.
|
||||
library;
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Design Request Create Page
|
||||
///
|
||||
/// Form with:
|
||||
/// - Progress steps indicator
|
||||
/// - Basic information (name, area, location, style, budget)
|
||||
/// - Detailed requirements (notes)
|
||||
/// - File upload with preview
|
||||
class DesignRequestCreatePage extends HookConsumerWidget {
|
||||
const DesignRequestCreatePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
final projectNameController = useTextEditingController();
|
||||
final areaController = useTextEditingController();
|
||||
final locationController = useTextEditingController();
|
||||
final notesController = useTextEditingController();
|
||||
|
||||
final selectedStyle = useState<String>('');
|
||||
final selectedBudget = useState<String>('');
|
||||
final selectedFiles = useState<List<PlatformFile>>([]);
|
||||
final isSubmitting = useState(false);
|
||||
|
||||
Future<void> pickFiles() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: true,
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf'],
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
// Validate file sizes
|
||||
final validFiles = <PlatformFile>[];
|
||||
for (final file in result.files) {
|
||||
if (file.size <= 10 * 1024 * 1024) { // 10MB max
|
||||
validFiles.add(file);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${file.name} quá lớn (tối đa 10MB)'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedFiles.value = [...selectedFiles.value, ...validFiles];
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Lỗi khi chọn file: $e'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void removeFile(int index) {
|
||||
final files = List<PlatformFile>.from(selectedFiles.value);
|
||||
files.removeAt(index);
|
||||
selectedFiles.value = files;
|
||||
}
|
||||
|
||||
Future<void> submitForm() async {
|
||||
if (formKey.currentState?.validate() ?? false) {
|
||||
isSubmitting.value = true;
|
||||
|
||||
// Simulate API call
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
isSubmitting.value = false;
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Yêu cầu thiết kế đã được gửi thành công!'),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate back
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Vui lòng kiểm tra lại thông tin'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.grey50,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.white,
|
||||
elevation: AppBarSpecs.elevation,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
centerTitle: false,
|
||||
title: const Text(
|
||||
'Tạo yêu cầu thiết kế mới',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
actions: const [
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
// Progress Steps
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_ProgressStep(number: 1, isActive: true),
|
||||
Container(
|
||||
width: 16,
|
||||
height: 2,
|
||||
color: AppColors.grey100,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
_ProgressStep(number: 2),
|
||||
Container(
|
||||
width: 16,
|
||||
height: 2,
|
||||
color: AppColors.grey100,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
_ProgressStep(number: 3),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Basic Information Card
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: AppColors.primaryBlue,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Thông tin cơ bản',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Project Name
|
||||
_FormField(
|
||||
label: 'Tên dự án/Khách hàng',
|
||||
required: true,
|
||||
controller: projectNameController,
|
||||
hint: 'VD: Thiết kế nhà anh Minh - Quận 7',
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Vui lòng nhập tên dự án';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Area
|
||||
_FormField(
|
||||
label: 'Diện tích (m²)',
|
||||
required: true,
|
||||
controller: areaController,
|
||||
hint: 'VD: 120',
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Vui lòng nhập diện tích';
|
||||
}
|
||||
final area = double.tryParse(value);
|
||||
if (area == null || area <= 0) {
|
||||
return 'Diện tích phải là số dương';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Location
|
||||
_FormField(
|
||||
label: 'Khu vực (Tỉnh/ Thành phố)',
|
||||
required: true,
|
||||
controller: locationController,
|
||||
hint: 'VD: Hà Nội',
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Vui lòng nhập khu vực';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Style
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: const TextSpan(
|
||||
text: 'Phong cách mong muốn',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' *',
|
||||
style: TextStyle(color: AppColors.danger),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
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),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
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ú)')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
selectedStyle.value = value ?? '';
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Vui lòng chọn phong cách';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Budget
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Ngân sách dự kiến',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
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),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
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')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
selectedBudget.value = value ?? '';
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Detailed Requirements Card
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
color: AppColors.primaryBlue,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Yêu cầu chi tiết',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Notes
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: const TextSpan(
|
||||
text: 'Ghi chú chi tiết',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' *',
|
||||
style: TextStyle(color: AppColors.danger),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
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...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Vui lòng mô tả yêu cầu chi tiết';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// File Upload Card
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_upload_outlined,
|
||||
color: AppColors.primaryBlue,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Đính kèm tài liệu',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Upload Area
|
||||
InkWell(
|
||||
onTap: pickFiles,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppColors.grey100,
|
||||
width: 2,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: AppColors.grey50,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_upload_outlined,
|
||||
size: 32,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Nhấn để chọn file hoặc kéo thả vào đây',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Hỗ trợ: JPG, PNG, PDF (Tối đa 10MB mỗi file)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// File Preview
|
||||
if (selectedFiles.value.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
...selectedFiles.value.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final file = entry.value;
|
||||
return _FilePreviewItem(
|
||||
file: file,
|
||||
onRemove: () => removeFile(index),
|
||||
);
|
||||
}),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Submit Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: isSubmitting.value ? null : submitForm,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
disabledBackgroundColor: AppColors.grey100,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: isSubmitting.value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColors.white,
|
||||
),
|
||||
)
|
||||
: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.send, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Gửi yêu cầu',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Progress Step Widget
|
||||
class _ProgressStep extends StatelessWidget {
|
||||
final int number;
|
||||
final bool isActive;
|
||||
final bool isCompleted;
|
||||
|
||||
const _ProgressStep({
|
||||
required this.number,
|
||||
this.isActive = false,
|
||||
this.isCompleted = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isCompleted
|
||||
? AppColors.success
|
||||
: isActive
|
||||
? AppColors.primaryBlue
|
||||
: AppColors.grey100,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$number',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isActive || isCompleted ? AppColors.white : AppColors.grey500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Form Field Widget
|
||||
class _FormField extends StatelessWidget {
|
||||
final String label;
|
||||
final bool required;
|
||||
final TextEditingController controller;
|
||||
final String hint;
|
||||
final TextInputType? keyboardType;
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
const _FormField({
|
||||
required this.label,
|
||||
this.required = false,
|
||||
required this.controller,
|
||||
required this.hint,
|
||||
this.keyboardType,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
children: required
|
||||
? const [
|
||||
TextSpan(
|
||||
text: ' *',
|
||||
style: TextStyle(color: AppColors.danger),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 2),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 2),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
validator: validator,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// File Preview Item Widget
|
||||
class _FilePreviewItem extends StatelessWidget {
|
||||
final PlatformFile file;
|
||||
final VoidCallback onRemove;
|
||||
|
||||
const _FilePreviewItem({
|
||||
required this.file,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
IconData _getFileIcon() {
|
||||
final extension = file.extension?.toLowerCase();
|
||||
if (extension == 'pdf') return Icons.picture_as_pdf;
|
||||
if (extension == 'jpg' || extension == 'jpeg' || extension == 'png') {
|
||||
return Icons.image;
|
||||
}
|
||||
return Icons.insert_drive_file;
|
||||
}
|
||||
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryBlue,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(
|
||||
_getFileIcon(),
|
||||
color: AppColors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
file.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_formatFileSize(file.size),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
color: AppColors.danger,
|
||||
onPressed: onRemove,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 24,
|
||||
minHeight: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,895 @@
|
||||
/// Page: Design Request Detail Page
|
||||
///
|
||||
/// Displays design request details following html/design-request-detail.html.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
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/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart';
|
||||
|
||||
/// Design Request Detail Page
|
||||
///
|
||||
/// Shows complete details of a design request including:
|
||||
/// - Request header with ID, date, and status
|
||||
/// - Project information grid (area, style, budget, status)
|
||||
/// - Completion highlight (if completed) with 3D design link
|
||||
/// - Project details (name, description, contact, files)
|
||||
/// - Status timeline
|
||||
/// - Action buttons (edit, contact)
|
||||
class DesignRequestDetailPage extends ConsumerWidget {
|
||||
const DesignRequestDetailPage({
|
||||
required this.requestId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String requestId;
|
||||
|
||||
// Mock data - in real app, this would come from a provider
|
||||
Map<String, dynamic> _getRequestData() {
|
||||
final mockData = {
|
||||
'YC001': {
|
||||
'id': 'YC001',
|
||||
'name': 'Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)',
|
||||
'area': '120m²',
|
||||
'style': 'Hiện đại',
|
||||
'budget': '300-500 triệu',
|
||||
'status': DesignRequestStatus.completed,
|
||||
'statusText': 'Đã hoàn thành',
|
||||
'description':
|
||||
'Thiết kế nhà phố 3 tầng phong cách hiện đại với 4 phòng ngủ, 3 phòng tắm, phòng khách rộng rãi và khu bếp mở. '
|
||||
'Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng. '
|
||||
'Tầng 1: garage, phòng khách, bếp. '
|
||||
'Tầng 2: 2 phòng ngủ, 2 phòng tắm. '
|
||||
'Tầng 3: phòng ngủ master, phòng làm việc, sân thượng.',
|
||||
'contact': 'SĐT: 0901234567 | Email: minh.nguyen@email.com',
|
||||
'createdDate': '20/10/2024',
|
||||
'files': ['mat-bang-hien-tai.jpg', 'ban-ve-kien-truc.dwg'],
|
||||
'designLink': 'https://example.com/3d-design/YC001',
|
||||
'timeline': [
|
||||
{
|
||||
'title': 'Thiết kế hoàn thành',
|
||||
'description': 'File thiết kế 3D đã được gửi đến khách hàng',
|
||||
'date': '25/10/2024 - 14:30',
|
||||
'status': DesignRequestStatus.completed,
|
||||
},
|
||||
{
|
||||
'title': 'Bắt đầu thiết kế',
|
||||
'description': 'KTS Nguyễn Văn An đã nhận và bắt đầu thiết kế',
|
||||
'date': '22/10/2024 - 09:00',
|
||||
'status': DesignRequestStatus.designing,
|
||||
},
|
||||
{
|
||||
'title': 'Tiếp nhận yêu cầu',
|
||||
'description': 'Yêu cầu thiết kế đã được tiếp nhận và xem xét',
|
||||
'date': '20/10/2024 - 16:45',
|
||||
'status': DesignRequestStatus.pending,
|
||||
},
|
||||
{
|
||||
'title': 'Gửi yêu cầu',
|
||||
'description': 'Yêu cầu thiết kế đã được gửi thành công',
|
||||
'date': '20/10/2024 - 16:30',
|
||||
'status': DesignRequestStatus.pending,
|
||||
},
|
||||
],
|
||||
},
|
||||
'YC002': {
|
||||
'id': 'YC002',
|
||||
'name': 'Cải tạo căn hộ chung cư - Chị Lan (Quận 2)',
|
||||
'area': '85m²',
|
||||
'style': 'Scandinavian',
|
||||
'budget': '100-300 triệu',
|
||||
'status': DesignRequestStatus.designing,
|
||||
'statusText': 'Đang thiết kế',
|
||||
'description':
|
||||
'Cải tạo căn hộ chung cư 3PN theo phong cách Scandinavian. '
|
||||
'Tối ưu không gian lưu trữ, sử dụng gạch men màu sáng và gỗ tự nhiên.',
|
||||
'contact': 'SĐT: 0987654321',
|
||||
'createdDate': '25/10/2024',
|
||||
'files': ['hinh-anh-hien-trang.jpg'],
|
||||
'designLink': null,
|
||||
'timeline': [
|
||||
{
|
||||
'title': 'Bắt đầu thiết kế',
|
||||
'description': 'KTS đã nhận và đang tiến hành thiết kế',
|
||||
'date': '26/10/2024 - 10:00',
|
||||
'status': DesignRequestStatus.designing,
|
||||
},
|
||||
{
|
||||
'title': 'Tiếp nhận yêu cầu',
|
||||
'description': 'Yêu cầu thiết kế đã được tiếp nhận',
|
||||
'date': '25/10/2024 - 14:30',
|
||||
'status': DesignRequestStatus.pending,
|
||||
},
|
||||
{
|
||||
'title': 'Gửi yêu cầu',
|
||||
'description': 'Yêu cầu thiết kế đã được gửi thành công',
|
||||
'date': '25/10/2024 - 14:15',
|
||||
'status': DesignRequestStatus.pending,
|
||||
},
|
||||
],
|
||||
},
|
||||
'YC003': {
|
||||
'id': 'YC003',
|
||||
'name': 'Thiết kế biệt thự 2 tầng - Anh Đức (Bình Dương)',
|
||||
'area': '200m²',
|
||||
'style': 'Luxury',
|
||||
'budget': 'Trên 1 tỷ',
|
||||
'status': DesignRequestStatus.pending,
|
||||
'statusText': 'Chờ tiếp nhận',
|
||||
'description':
|
||||
'Thiết kế biệt thự 2 tầng phong cách luxury với hồ bơi và sân vườn. '
|
||||
'5 phòng ngủ, 4 phòng tắm, phòng giải trí và garage 2 xe.',
|
||||
'contact': 'SĐT: 0923456789 | Email: duc.le@gmail.com',
|
||||
'createdDate': '28/10/2024',
|
||||
'files': ['mat-bang-dat.pdf', 'y-tuong-thiet-ke.jpg'],
|
||||
'designLink': null,
|
||||
'timeline': [
|
||||
{
|
||||
'title': 'Gửi yêu cầu',
|
||||
'description': 'Yêu cầu thiết kế đã được gửi thành công',
|
||||
'date': '28/10/2024 - 11:20',
|
||||
'status': DesignRequestStatus.pending,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return mockData[requestId] ?? mockData['YC001']!;
|
||||
}
|
||||
|
||||
Color _getStatusColor(DesignRequestStatus status) {
|
||||
switch (status) {
|
||||
case DesignRequestStatus.pending:
|
||||
return const Color(0xFFffc107);
|
||||
case DesignRequestStatus.designing:
|
||||
return const Color(0xFF3730a3);
|
||||
case DesignRequestStatus.completed:
|
||||
return const Color(0xFF065f46);
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusBackgroundColor(DesignRequestStatus status) {
|
||||
switch (status) {
|
||||
case DesignRequestStatus.pending:
|
||||
return const Color(0xFFfef3c7);
|
||||
case DesignRequestStatus.designing:
|
||||
return const Color(0xFFe0e7ff);
|
||||
case DesignRequestStatus.completed:
|
||||
return const Color(0xFFd1fae5);
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getTimelineIcon(DesignRequestStatus status, int index) {
|
||||
if (status == DesignRequestStatus.completed) {
|
||||
return Icons.check;
|
||||
} else if (status == DesignRequestStatus.designing) {
|
||||
return Icons.architecture;
|
||||
} else {
|
||||
return index == 0 ? Icons.send : Icons.access_time;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getFileIcon(String fileName) {
|
||||
final extension = fileName.split('.').last.toLowerCase();
|
||||
|
||||
if (['jpg', 'jpeg', 'png', 'gif'].contains(extension)) {
|
||||
return Icons.image;
|
||||
} else if (extension == 'pdf') {
|
||||
return Icons.picture_as_pdf;
|
||||
} else if (extension == 'dwg') {
|
||||
return Icons.architecture;
|
||||
} else if (['doc', 'docx'].contains(extension)) {
|
||||
return Icons.description;
|
||||
}
|
||||
return Icons.insert_drive_file;
|
||||
}
|
||||
|
||||
Future<void> _viewDesign3D(BuildContext context, String? designLink) async {
|
||||
if (designLink != null) {
|
||||
final uri = Uri.parse(designLink);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Không thể mở link thiết kế 3D'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Link thiết kế 3D chưa có sẵn'),
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _editRequest(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Chức năng chỉnh sửa yêu cầu sẽ được triển khai trong phiên bản tiếp theo',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _contactSupport(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Liên hệ hỗ trợ'),
|
||||
content: const Text(
|
||||
'Bạn có muốn liên hệ hỗ trợ về yêu cầu thiết kế này?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Hủy'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
context.push(RouteNames.chat);
|
||||
},
|
||||
child: const Text('Liên hệ'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _shareRequest(BuildContext context, String requestId, String name) async {
|
||||
try {
|
||||
await Share.share(
|
||||
'Yêu cầu thiết kế #$requestId\n$name',
|
||||
subject: 'Chia sẻ yêu cầu thiết kế',
|
||||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Lỗi khi chia sẻ: $e'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final request = _getRequestData();
|
||||
final status = request['status'] as DesignRequestStatus;
|
||||
final timeline = request['timeline'] as List<Map<String, dynamic>>;
|
||||
final files = request['files'] as List<String>;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.grey50,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.white,
|
||||
elevation: AppBarSpecs.elevation,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
centerTitle: false,
|
||||
title: const Text(
|
||||
'Chi tiết Yêu cầu',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share, color: Colors.black),
|
||||
onPressed: () => _shareRequest(
|
||||
context,
|
||||
request['id'] as String,
|
||||
request['name'] as String,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
// Request Header Card
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
// Request ID and Date
|
||||
Text(
|
||||
'#${request['id']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Ngày gửi: ${request['createdDate']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Status Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusBackgroundColor(status),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
request['statusText'] as String,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _getStatusColor(status),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Info Grid
|
||||
_InfoGrid(
|
||||
area: request['area'] as String,
|
||||
style: request['style'] as String,
|
||||
budget: request['budget'] as String,
|
||||
statusText: request['statusText'] as String,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Completion Highlight (only if completed)
|
||||
if (status == DesignRequestStatus.completed)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFd1fae5), Color(0xFFa7f3d0)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
border: Border.all(color: const Color(0xFF10b981), width: 2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'🎉 Thiết kế đã hoàn thành!',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF065f46),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Thiết kế 3D của bạn đã sẵn sàng để xem',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF065f46),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _viewDesign3D(
|
||||
context,
|
||||
request['designLink'] as String?,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF10b981),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.view_in_ar, color: Colors.white),
|
||||
label: const Text(
|
||||
'Xem Link Thiết kế 3D',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (status == DesignRequestStatus.completed)
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Project Details Card
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Project Name
|
||||
_SectionHeader(
|
||||
icon: Icons.info,
|
||||
title: 'Thông tin dự án',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
height: 1.6,
|
||||
),
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: 'Tên dự án: ',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextSpan(text: request['name'] as String),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Description
|
||||
_SectionHeader(
|
||||
icon: Icons.edit,
|
||||
title: 'Mô tả yêu cầu',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
request['description'] as String,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
height: 1.6,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Contact Info
|
||||
_SectionHeader(
|
||||
icon: Icons.phone,
|
||||
title: 'Thông tin liên hệ',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
request['contact'] as String,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
height: 1.6,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Files
|
||||
_SectionHeader(
|
||||
icon: Icons.attach_file,
|
||||
title: 'Tài liệu đính kèm',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (files.isEmpty)
|
||||
const Text(
|
||||
'Không có tài liệu đính kèm',
|
||||
style: TextStyle(
|
||||
color: AppColors.grey500,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
)
|
||||
else
|
||||
...files.map(
|
||||
(file) => _FileItem(
|
||||
fileName: file,
|
||||
icon: _getFileIcon(file),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Timeline Card
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_SectionHeader(
|
||||
icon: Icons.history,
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _editRequest(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.grey900,
|
||||
side: const BorderSide(color: AppColors.grey100, width: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text(
|
||||
'Chỉnh sửa',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _contactSupport(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.chat_bubble),
|
||||
label: const Text(
|
||||
'Liên hệ',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Info Grid Widget
|
||||
class _InfoGrid extends StatelessWidget {
|
||||
const _InfoGrid({
|
||||
required this.area,
|
||||
required this.style,
|
||||
required this.budget,
|
||||
required this.statusText,
|
||||
});
|
||||
|
||||
final String area;
|
||||
final String style;
|
||||
final String budget;
|
||||
final String statusText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _InfoItem(label: 'Diện tích', value: area),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _InfoItem(label: 'Phong cách', value: style),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _InfoItem(label: 'Ngân sách', value: budget),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _InfoItem(label: 'Trạng thái', value: statusText),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Info Item Widget
|
||||
class _InfoItem extends StatelessWidget {
|
||||
const _InfoItem({
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Section Header Widget
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.primaryBlue, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// File Item Widget
|
||||
class _FileItem extends StatelessWidget {
|
||||
const _FileItem({
|
||||
required this.fileName,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final String fileName;
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryBlue,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(icon, color: Colors.white, size: 16),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
fileName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Timeline Item Widget
|
||||
class _TimelineItem extends StatelessWidget {
|
||||
const _TimelineItem({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.date,
|
||||
required this.status,
|
||||
required this.icon,
|
||||
required this.isLast,
|
||||
required this.getStatusColor,
|
||||
required this.getStatusBackgroundColor,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final String date;
|
||||
final DesignRequestStatus status;
|
||||
final IconData icon;
|
||||
final bool isLast;
|
||||
final Color Function(DesignRequestStatus) getStatusColor;
|
||||
final Color Function(DesignRequestStatus) getStatusBackgroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Icon and line
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: getStatusBackgroundColor(status),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: getStatusColor(status),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
if (!isLast)
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 2,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
color: AppColors.grey100,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Content
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: isLast ? 0 : 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
date,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ library;
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
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';
|
||||
|
||||
/// Model Houses Page
|
||||
@@ -73,11 +75,7 @@ class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
|
||||
}
|
||||
|
||||
void _createNewRequest() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Chức năng tạo yêu cầu thiết kế sẽ được triển khai trong phiên bản tiếp theo'),
|
||||
),
|
||||
);
|
||||
context.push(RouteNames.designRequestCreate);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -446,11 +444,7 @@ class _RequestCard extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Chức năng xem chi tiết yêu cầu sẽ được triển khai trong phiên bản tiếp theo'),
|
||||
),
|
||||
);
|
||||
context.push('/model-houses/design-request/${code.replaceAll('#', '')}');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
|
||||
Reference in New Issue
Block a user