add model/design
This commit is contained in:
@@ -30,6 +30,8 @@ import 'package:worker/features/products/presentation/pages/product_detail_page.
|
|||||||
import 'package:worker/features/products/presentation/pages/products_page.dart';
|
import 'package:worker/features/products/presentation/pages/products_page.dart';
|
||||||
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
|
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
|
||||||
import 'package:worker/features/quotes/presentation/pages/quotes_page.dart';
|
import 'package:worker/features/quotes/presentation/pages/quotes_page.dart';
|
||||||
|
import 'package:worker/features/showrooms/presentation/pages/design_request_create_page.dart';
|
||||||
|
import 'package:worker/features/showrooms/presentation/pages/design_request_detail_page.dart';
|
||||||
import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart';
|
import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart';
|
||||||
|
|
||||||
/// App Router
|
/// App Router
|
||||||
@@ -272,6 +274,27 @@ class AppRouter {
|
|||||||
MaterialPage(key: state.pageKey, child: const ModelHousesPage()),
|
MaterialPage(key: state.pageKey, child: const ModelHousesPage()),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Design Request Create Route
|
||||||
|
GoRoute(
|
||||||
|
path: RouteNames.designRequestCreate,
|
||||||
|
name: RouteNames.designRequestCreate,
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
MaterialPage(key: state.pageKey, child: const DesignRequestCreatePage()),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Design Request Detail Route
|
||||||
|
GoRoute(
|
||||||
|
path: RouteNames.designRequestDetail,
|
||||||
|
name: RouteNames.designRequestDetail,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
final requestId = state.pathParameters['id'];
|
||||||
|
return MaterialPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: DesignRequestDetailPage(requestId: requestId ?? 'YC001'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// TODO: Add more routes as features are implemented
|
// TODO: Add more routes as features are implemented
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -396,8 +419,10 @@ class RouteNames {
|
|||||||
// Chat Route
|
// Chat Route
|
||||||
static const String chat = '/chat';
|
static const String chat = '/chat';
|
||||||
|
|
||||||
// Model Houses Route
|
// Model Houses & Design Requests Routes
|
||||||
static const String modelHouses = '/model-houses';
|
static const String modelHouses = '/model-houses';
|
||||||
|
static const String designRequestCreate = '/model-houses/design-request/create';
|
||||||
|
static const String designRequestDetail = '/model-houses/design-request/:id';
|
||||||
|
|
||||||
// Authentication Routes (TODO: implement when auth feature is ready)
|
// Authentication Routes (TODO: implement when auth feature is ready)
|
||||||
static const String login = '/login';
|
static const String login = '/login';
|
||||||
|
|||||||
@@ -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:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/constants/ui_constants.dart';
|
||||||
|
import 'package:worker/core/router/app_router.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
|
||||||
/// Model Houses Page
|
/// Model Houses Page
|
||||||
@@ -73,11 +75,7 @@ class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _createNewRequest() {
|
void _createNewRequest() {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
context.push(RouteNames.designRequestCreate);
|
||||||
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'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -446,11 +444,7 @@ class _RequestCard extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
context.push('/model-houses/design-request/${code.replaceAll('#', '')}');
|
||||||
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'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
40
pubspec.lock
40
pubspec.lock
@@ -377,6 +377,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.7"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1353,6 +1361,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.24"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.5"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1361,6 +1393,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.2.1"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.4"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ dependencies:
|
|||||||
intl: ^0.20.0
|
intl: ^0.20.0
|
||||||
share_plus: ^9.0.0
|
share_plus: ^9.0.0
|
||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
|
file_picker: ^8.0.0
|
||||||
|
url_launcher: ^6.3.0
|
||||||
path_provider: ^2.1.3
|
path_provider: ^2.1.3
|
||||||
shared_preferences: ^2.2.3
|
shared_preferences: ^2.2.3
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user