create submission
This commit is contained in:
@@ -11,6 +11,9 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/projects/data/models/project_submission_request.dart';
|
||||
import 'package:worker/features/projects/domain/entities/project_progress.dart';
|
||||
import 'package:worker/features/projects/presentation/providers/submissions_provider.dart';
|
||||
|
||||
/// Project Submission Create Page
|
||||
class SubmissionCreatePage extends ConsumerStatefulWidget {
|
||||
@@ -35,10 +38,10 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
final _descriptionController = TextEditingController();
|
||||
|
||||
// Form state
|
||||
String? _selectedProgress;
|
||||
ProjectProgress? _selectedProgress;
|
||||
DateTime? _expectedStartDate;
|
||||
final List<File> _uploadedFiles = [];
|
||||
bool _showStartDateField = false;
|
||||
bool _isSubmitting = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -217,10 +220,8 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
|
||||
_buildProgressDropdown(),
|
||||
|
||||
if (_showStartDateField) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildDateField(),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
_buildExpectedDateField(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -434,6 +435,8 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
}
|
||||
|
||||
Widget _buildProgressDropdown() {
|
||||
final progressListAsync = ref.watch(projectProgressListProvider);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -457,68 +460,93 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedProgress,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100),
|
||||
progressListAsync.when(
|
||||
data: (progressList) => DropdownButtonFormField<ProjectProgress>(
|
||||
initialValue: _selectedProgress,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
hint: const Text('Chọn tiến độ'),
|
||||
items: progressList
|
||||
.map((progress) => DropdownMenuItem<ProjectProgress>(
|
||||
value: progress,
|
||||
child: Text(progress.status),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedProgress = value;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return 'Vui lòng chọn tiến độ công trình';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
loading: () => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
border: Border.all(color: AppColors.grey100),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
child: const Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Text('Đang tải...', style: TextStyle(color: AppColors.grey500)),
|
||||
],
|
||||
),
|
||||
),
|
||||
hint: const Text('Chọn tiến độ'),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 'not-started',
|
||||
child: Text('Chưa khởi công'),
|
||||
error: (error, _) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
border: Border.all(color: AppColors.danger),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'foundation',
|
||||
child: Text('Khởi công móng'),
|
||||
child: Row(
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.circleExclamation,
|
||||
size: 16, color: AppColors.danger),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text('Không thể tải danh sách tiến độ',
|
||||
style: TextStyle(color: AppColors.danger)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
ref.invalidate(projectProgressListProvider),
|
||||
child: const Text('Thử lại'),
|
||||
),
|
||||
],
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'rough-construction',
|
||||
child: Text('Đang phần thô'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'finishing',
|
||||
child: Text('Đang hoàn thiện'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'topped-out',
|
||||
child: Text('Cất nóc'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedProgress = value;
|
||||
_showStartDateField = value == 'not-started';
|
||||
if (!_showStartDateField) {
|
||||
_expectedStartDate = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Vui lòng chọn tiến độ công trình';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateField() {
|
||||
Widget _buildExpectedDateField() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -532,7 +560,7 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: _pickDate,
|
||||
onTap: _pickExpectedDate,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -545,7 +573,7 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
children: [
|
||||
Text(
|
||||
_expectedStartDate != null
|
||||
? '${_expectedStartDate!.day}/${_expectedStartDate!.month}/${_expectedStartDate!.year}'
|
||||
? '${_expectedStartDate!.day.toString().padLeft(2, '0')}/${_expectedStartDate!.month.toString().padLeft(2, '0')}/${_expectedStartDate!.year}'
|
||||
: 'Chọn ngày',
|
||||
style: TextStyle(
|
||||
color: _expectedStartDate != null
|
||||
@@ -571,35 +599,89 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
final fileSizeInBytes = file.lengthSync();
|
||||
final fileSizeInMB = (fileSizeInBytes / (1024 * 1024)).toStringAsFixed(2);
|
||||
|
||||
// Get upload state for this file
|
||||
final uploadStates = ref.watch(uploadProjectFilesProvider);
|
||||
final isUploading = index < uploadStates.length && uploadStates[index].isUploading;
|
||||
final isUploaded = index < uploadStates.length && uploadStates[index].isUploaded;
|
||||
final hasError = index < uploadStates.length && uploadStates[index].error != null;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
border: Border.all(color: AppColors.grey100),
|
||||
border: Border.all(
|
||||
color: hasError
|
||||
? AppColors.danger
|
||||
: isUploaded
|
||||
? AppColors.success
|
||||
: AppColors.grey100,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Image.file(
|
||||
file,
|
||||
width: 48,
|
||||
height: 48,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
// Image with upload overlay
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Image.file(
|
||||
file,
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: AppColors.grey100,
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.image,
|
||||
size: 24,
|
||||
color: AppColors.grey500,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: AppColors.grey100,
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.image,
|
||||
size: 24,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Uploading overlay
|
||||
if (isUploading)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Uploaded checkmark overlay
|
||||
if (isUploaded)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.success.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Center(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.check,
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -617,27 +699,39 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${fileSizeInMB}MB',
|
||||
style: const TextStyle(
|
||||
isUploading
|
||||
? 'Đang tải lên...'
|
||||
: isUploaded
|
||||
? 'Đã tải lên'
|
||||
: hasError
|
||||
? 'Lỗi tải lên'
|
||||
: '${fileSizeInMB}MB',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
color: hasError
|
||||
? AppColors.danger
|
||||
: isUploaded
|
||||
? AppColors.success
|
||||
: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.xmark,
|
||||
size: 16,
|
||||
color: AppColors.danger,
|
||||
// Only show remove button when not uploading
|
||||
if (!_isSubmitting)
|
||||
IconButton(
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.xmark,
|
||||
size: 16,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_uploadedFiles.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_uploadedFiles.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -648,39 +742,50 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: _handleSubmit,
|
||||
onPressed: _isSubmitting ? null : _handleSubmit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
disabledBackgroundColor: AppColors.primaryBlue.withValues(alpha: 0.6),
|
||||
disabledForegroundColor: AppColors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FaIcon(FontAwesomeIcons.paperPlane, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Gửi đăng ký',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.white),
|
||||
),
|
||||
)
|
||||
: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FaIcon(FontAwesomeIcons.paperPlane, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Gửi đăng ký',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickDate() async {
|
||||
Future<void> _pickExpectedDate() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(),
|
||||
initialDate: _expectedStartDate ?? DateTime.now(),
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365 * 3)),
|
||||
);
|
||||
|
||||
if (date != null) {
|
||||
@@ -725,34 +830,106 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
}
|
||||
|
||||
Future<void> _handleSubmit() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Xác nhận'),
|
||||
content: const Text('Xác nhận gửi đăng ký công trình?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Hủy'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Xác nhận'),
|
||||
),
|
||||
],
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
// Validate progress selection
|
||||
if (_selectedProgress == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Vui lòng chọn tiến độ công trình'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmed == true && mounted) {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Xác nhận'),
|
||||
content: const Text('Xác nhận gửi đăng ký công trình?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Hủy'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Xác nhận'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
try {
|
||||
// Parse area as double
|
||||
final area = double.tryParse(_areaController.text.trim()) ?? 0.0;
|
||||
|
||||
// Create submission request
|
||||
final request = ProjectSubmissionRequest(
|
||||
designedArea: _projectNameController.text.trim(),
|
||||
addressOfProject: _addressController.text.trim(),
|
||||
projectOwner: _ownerController.text.trim(),
|
||||
designFirm: _designUnitController.text.trim().isNotEmpty
|
||||
? _designUnitController.text.trim()
|
||||
: null,
|
||||
contractionContractor: _constructionUnitController.text.trim().isNotEmpty
|
||||
? _constructionUnitController.text.trim()
|
||||
: null,
|
||||
designArea: area,
|
||||
productsIncludedInTheDesign: _productsController.text.trim(),
|
||||
projectProgress: _selectedProgress!.id, // Use ProjectProgress.id (name from API)
|
||||
expectedCommencementDate: _expectedStartDate,
|
||||
description: _descriptionController.text.trim().isNotEmpty
|
||||
? _descriptionController.text.trim()
|
||||
: null,
|
||||
requestDate: DateTime.now(),
|
||||
);
|
||||
|
||||
// Step 1: Save project and get project name
|
||||
final projectName = await ref.read(saveSubmissionProvider.notifier).save(request);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Step 2: Upload files if any
|
||||
if (_uploadedFiles.isNotEmpty) {
|
||||
// Initialize upload provider with file paths
|
||||
final filePaths = _uploadedFiles.map((f) => f.path).toList();
|
||||
ref.read(uploadProjectFilesProvider.notifier).initFiles(filePaths);
|
||||
|
||||
// Upload all files
|
||||
await ref.read(uploadProjectFilesProvider.notifier).uploadAll(projectName);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Đăng ký công trình đã được gửi thành công!\nChúng tôi sẽ xem xét và liên hệ với bạn sớm nhất.',
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context, true); // Return true to indicate success
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Lỗi: ${e.toString().replaceAll('Exception: ', '')}'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isSubmitting = false);
|
||||
// Clear upload state
|
||||
ref.read(uploadProjectFilesProvider.notifier).clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user