submission
This commit is contained in:
@@ -5,6 +5,7 @@ library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
@@ -13,11 +14,17 @@ 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/domain/entities/project_submission.dart';
|
||||
import 'package:worker/features/projects/presentation/providers/submissions_provider.dart';
|
||||
|
||||
/// Project Submission Create Page
|
||||
/// Project Submission Create/Edit Page
|
||||
class SubmissionCreatePage extends ConsumerStatefulWidget {
|
||||
const SubmissionCreatePage({super.key});
|
||||
const SubmissionCreatePage({super.key, this.submission});
|
||||
|
||||
/// Optional submission for editing mode
|
||||
/// If null, creates new submission
|
||||
/// If provided, prefills form and updates existing submission
|
||||
final ProjectSubmission? submission;
|
||||
|
||||
@override
|
||||
ConsumerState<SubmissionCreatePage> createState() =>
|
||||
@@ -40,8 +47,73 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
// Form state
|
||||
ProjectProgress? _selectedProgress;
|
||||
DateTime? _expectedStartDate;
|
||||
final List<File> _uploadedFiles = [];
|
||||
final List<File> _uploadedFiles = []; // New files to upload
|
||||
List<ProjectFile> _existingFiles = []; // Existing files from API
|
||||
bool _isSubmitting = false;
|
||||
bool _isLoadingDetail = false;
|
||||
|
||||
/// Whether we're editing an existing submission
|
||||
bool get isEditing => widget.submission != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Fetch full detail when editing
|
||||
if (isEditing) {
|
||||
_loadDetail();
|
||||
}
|
||||
}
|
||||
|
||||
/// Load full project detail from API for editing
|
||||
Future<void> _loadDetail() async {
|
||||
if (!isEditing) return;
|
||||
|
||||
setState(() => _isLoadingDetail = true);
|
||||
|
||||
try {
|
||||
final detail = await ref.read(
|
||||
submissionDetailProvider(widget.submission!.submissionId).future,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Prefill form fields from entity
|
||||
_projectNameController.text = detail.designedArea;
|
||||
_addressController.text = detail.addressOfProject ?? '';
|
||||
_ownerController.text = detail.projectOwner ?? '';
|
||||
_designUnitController.text = detail.designFirm ?? '';
|
||||
_constructionUnitController.text = detail.constructionContractor ?? '';
|
||||
_areaController.text = detail.designArea.toString();
|
||||
_productsController.text = detail.productsIncludedInTheDesign ?? '';
|
||||
_descriptionController.text = detail.description ?? '';
|
||||
|
||||
// Set expected commencement date
|
||||
_expectedStartDate = detail.expectedCommencementDate;
|
||||
|
||||
// Find matching progress from the list
|
||||
final progressId = detail.projectProgress;
|
||||
if (progressId != null) {
|
||||
final progressList = await ref.read(projectProgressListProvider.future);
|
||||
_selectedProgress = progressList.where((p) => p.id == progressId).firstOrNull;
|
||||
}
|
||||
|
||||
// Set existing files from API
|
||||
_existingFiles = detail.filesList;
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Lỗi tải thông tin: $e'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isLoadingDetail = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -69,9 +141,9 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: const Text(
|
||||
'Đăng ký Công trình',
|
||||
style: TextStyle(color: Colors.black),
|
||||
title: Text(
|
||||
isEditing ? 'Chỉnh sửa Dự án' : 'Đăng ký Công trình',
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
@@ -88,7 +160,21 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
backgroundColor: AppColors.white,
|
||||
centerTitle: false,
|
||||
),
|
||||
body: Form(
|
||||
body: _isLoadingDetail
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Đang tải thông tin dự án...',
|
||||
style: TextStyle(color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(4),
|
||||
@@ -322,8 +408,41 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
),
|
||||
),
|
||||
|
||||
// Existing files from API
|
||||
if (_existingFiles.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Ảnh đã tải lên',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
..._existingFiles.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final file = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _buildExistingFileItem(file, index),
|
||||
);
|
||||
}),
|
||||
],
|
||||
|
||||
// New files to upload
|
||||
if (_uploadedFiles.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
if (_existingFiles.isNotEmpty)
|
||||
const Text(
|
||||
'Ảnh mới',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
if (_existingFiles.isNotEmpty) const SizedBox(height: 8),
|
||||
..._uploadedFiles.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final file = entry.value;
|
||||
@@ -737,6 +856,88 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExistingFileItem(ProjectFile file, int index) {
|
||||
final fileName = file.fileUrl.split('/').last;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
border: Border.all(color: AppColors.success),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Network image
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: file.fileUrl,
|
||||
width: 48,
|
||||
height: 48,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: AppColors.grey100,
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: AppColors.grey100,
|
||||
child: const Center(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.image,
|
||||
size: 24,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
fileName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'Đã tải lên',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.success,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Checkmark icon
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.circleCheck,
|
||||
size: 16,
|
||||
color: AppColors.success,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubmitButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -847,7 +1048,11 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
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?'),
|
||||
content: Text(
|
||||
isEditing
|
||||
? 'Xác nhận cập nhật thông tin dự án?'
|
||||
: 'Xác nhận gửi đăng ký công trình?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
@@ -870,7 +1075,9 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
final area = double.tryParse(_areaController.text.trim()) ?? 0.0;
|
||||
|
||||
// Create submission request
|
||||
// Include name field when editing (for update)
|
||||
final request = ProjectSubmissionRequest(
|
||||
name: isEditing ? widget.submission!.submissionId : null,
|
||||
designedArea: _projectNameController.text.trim(),
|
||||
addressOfProject: _addressController.text.trim(),
|
||||
projectOwner: _ownerController.text.trim(),
|
||||
@@ -907,9 +1114,11 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
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.',
|
||||
isEditing
|
||||
? 'Cập nhật dự án thành công!'
|
||||
: 'Đă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,
|
||||
),
|
||||
|
||||
@@ -171,7 +171,7 @@ class SubmissionsPage extends ConsumerWidget {
|
||||
itemCount: submissions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final submission = submissions[index];
|
||||
return _buildSubmissionCard(context, submission);
|
||||
return _buildSubmissionCard(context, ref, submission);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -260,17 +260,22 @@ class SubmissionsPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubmissionCard(BuildContext context, ProjectSubmission submission) {
|
||||
Widget _buildSubmissionCard(BuildContext context, WidgetRef ref, ProjectSubmission submission) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// TODO: Navigate to submission detail
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Chi tiết dự án ${submission.submissionId}')),
|
||||
onTap: () async {
|
||||
// Navigate to edit submission page
|
||||
final result = await context.push<bool>(
|
||||
RouteNames.submissionCreate,
|
||||
extra: submission,
|
||||
);
|
||||
if (result == true) {
|
||||
// Refresh submissions list after successful update
|
||||
ref.invalidate(allSubmissionsProvider);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
|
||||
Reference in New Issue
Block a user