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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,13 @@ class SubmissionsPage extends ConsumerWidget {
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.plus, color: Colors.black, size: 20),
|
||||
onPressed: () => context.push(RouteNames.submissionCreate),
|
||||
onPressed: () async {
|
||||
final result = await context.push<bool>(RouteNames.submissionCreate);
|
||||
if (result == true) {
|
||||
// Refresh submissions list after successful creation
|
||||
ref.invalidate(allSubmissionsProvider);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
|
||||
@@ -5,9 +5,12 @@ library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/features/projects/data/datasources/project_progress_local_datasource.dart';
|
||||
import 'package:worker/features/projects/data/datasources/project_status_local_datasource.dart';
|
||||
import 'package:worker/features/projects/data/datasources/submissions_remote_datasource.dart';
|
||||
import 'package:worker/features/projects/data/models/project_submission_request.dart';
|
||||
import 'package:worker/features/projects/data/repositories/submissions_repository_impl.dart';
|
||||
import 'package:worker/features/projects/domain/entities/project_progress.dart';
|
||||
import 'package:worker/features/projects/domain/entities/project_status.dart';
|
||||
import 'package:worker/features/projects/domain/entities/project_submission.dart';
|
||||
import 'package:worker/features/projects/domain/repositories/submissions_repository.dart';
|
||||
@@ -20,6 +23,12 @@ ProjectStatusLocalDataSource projectStatusLocalDataSource(Ref ref) {
|
||||
return ProjectStatusLocalDataSource();
|
||||
}
|
||||
|
||||
/// Project Progress Local Data Source Provider
|
||||
@riverpod
|
||||
ProjectProgressLocalDataSource projectProgressLocalDataSource(Ref ref) {
|
||||
return ProjectProgressLocalDataSource();
|
||||
}
|
||||
|
||||
/// Submissions Remote Data Source Provider
|
||||
@riverpod
|
||||
Future<SubmissionsRemoteDataSource> submissionsRemoteDataSource(Ref ref) async {
|
||||
@@ -32,7 +41,12 @@ Future<SubmissionsRemoteDataSource> submissionsRemoteDataSource(Ref ref) async {
|
||||
Future<SubmissionsRepository> submissionsRepository(Ref ref) async {
|
||||
final remoteDataSource = await ref.watch(submissionsRemoteDataSourceProvider.future);
|
||||
final statusLocalDataSource = ref.watch(projectStatusLocalDataSourceProvider);
|
||||
return SubmissionsRepositoryImpl(remoteDataSource, statusLocalDataSource);
|
||||
final progressLocalDataSource = ref.watch(projectProgressLocalDataSourceProvider);
|
||||
return SubmissionsRepositoryImpl(
|
||||
remoteDataSource,
|
||||
statusLocalDataSource,
|
||||
progressLocalDataSource,
|
||||
);
|
||||
}
|
||||
|
||||
/// Project Status List Provider
|
||||
@@ -57,16 +71,39 @@ class ProjectStatusList extends _$ProjectStatusList {
|
||||
}
|
||||
}
|
||||
|
||||
/// Project Progress List Provider
|
||||
///
|
||||
/// Fetches construction progress stages from API with cache-first pattern.
|
||||
/// Used for dropdown selection when creating/updating project submissions.
|
||||
@riverpod
|
||||
class ProjectProgressList extends _$ProjectProgressList {
|
||||
@override
|
||||
Future<List<ProjectProgress>> build() async {
|
||||
final repository = await ref.watch(submissionsRepositoryProvider.future);
|
||||
return repository.getProjectProgressList();
|
||||
}
|
||||
|
||||
/// Refresh progress list from remote (force refresh)
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final repository = await ref.read(submissionsRepositoryProvider.future);
|
||||
return repository.getProjectProgressList(forceRefresh: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// All Submissions Provider
|
||||
///
|
||||
/// Fetches and manages submissions data from remote.
|
||||
/// Waits for project status list to be loaded first.
|
||||
/// Waits for project status list and progress list to be loaded first.
|
||||
@riverpod
|
||||
class AllSubmissions extends _$AllSubmissions {
|
||||
@override
|
||||
Future<List<ProjectSubmission>> build() async {
|
||||
// Ensure status list is loaded first (for filter options)
|
||||
// Ensure status list and progress list are loaded first (for filter options)
|
||||
await ref.watch(projectStatusListProvider.future);
|
||||
await ref.watch(projectProgressListProvider.future);
|
||||
|
||||
// Then fetch submissions
|
||||
final repository = await ref.watch(submissionsRepositoryProvider.future);
|
||||
@@ -77,8 +114,9 @@ class AllSubmissions extends _$AllSubmissions {
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Also refresh status list
|
||||
// Also refresh status list and progress list
|
||||
await ref.read(projectStatusListProvider.notifier).refresh();
|
||||
await ref.read(projectProgressListProvider.notifier).refresh();
|
||||
|
||||
final repository = await ref.read(submissionsRepositoryProvider.future);
|
||||
return repository.getSubmissions();
|
||||
@@ -154,3 +192,152 @@ AsyncValue<List<ProjectSubmission>> filteredSubmissions(Ref ref) {
|
||||
return filtered;
|
||||
});
|
||||
}
|
||||
|
||||
/// Save Submission Provider
|
||||
///
|
||||
/// Handles creating new project submissions via API.
|
||||
@riverpod
|
||||
class SaveSubmission extends _$SaveSubmission {
|
||||
@override
|
||||
AsyncValue<void> build() {
|
||||
return const AsyncValue.data(null);
|
||||
}
|
||||
|
||||
/// Save a new project submission
|
||||
///
|
||||
/// Returns the project name (ID) if successful, throws exception on failure.
|
||||
Future<String> save(ProjectSubmissionRequest request) async {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final repository = await ref.read(submissionsRepositoryProvider.future);
|
||||
if (!ref.mounted) throw Exception('Provider disposed');
|
||||
|
||||
final projectName = await repository.saveSubmission(request);
|
||||
if (!ref.mounted) return projectName;
|
||||
|
||||
state = const AsyncValue.data(null);
|
||||
|
||||
// Refresh submissions list after successful save
|
||||
ref.invalidate(allSubmissionsProvider);
|
||||
|
||||
return projectName;
|
||||
} catch (e, st) {
|
||||
if (ref.mounted) {
|
||||
state = AsyncValue.error(e, st);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload state for tracking individual file uploads
|
||||
class FileUploadState {
|
||||
final String filePath;
|
||||
final bool isUploading;
|
||||
final bool isUploaded;
|
||||
final String? fileUrl;
|
||||
final String? error;
|
||||
|
||||
const FileUploadState({
|
||||
required this.filePath,
|
||||
this.isUploading = false,
|
||||
this.isUploaded = false,
|
||||
this.fileUrl,
|
||||
this.error,
|
||||
});
|
||||
|
||||
FileUploadState copyWith({
|
||||
bool? isUploading,
|
||||
bool? isUploaded,
|
||||
String? fileUrl,
|
||||
String? error,
|
||||
}) {
|
||||
return FileUploadState(
|
||||
filePath: filePath,
|
||||
isUploading: isUploading ?? this.isUploading,
|
||||
isUploaded: isUploaded ?? this.isUploaded,
|
||||
fileUrl: fileUrl ?? this.fileUrl,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload Project Files Provider
|
||||
///
|
||||
/// Handles uploading multiple files for a project submission.
|
||||
/// Tracks upload state for each file individually.
|
||||
@riverpod
|
||||
class UploadProjectFiles extends _$UploadProjectFiles {
|
||||
@override
|
||||
List<FileUploadState> build() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Initialize with file paths
|
||||
void initFiles(List<String> filePaths) {
|
||||
state = filePaths
|
||||
.map((path) => FileUploadState(filePath: path))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Upload all files for a project
|
||||
/// Returns list of uploaded file URLs
|
||||
Future<List<String>> uploadAll(String projectName) async {
|
||||
final uploadedUrls = <String>[];
|
||||
|
||||
for (var i = 0; i < state.length; i++) {
|
||||
if (!ref.mounted) break;
|
||||
|
||||
// Mark as uploading
|
||||
state = [
|
||||
...state.sublist(0, i),
|
||||
state[i].copyWith(isUploading: true),
|
||||
...state.sublist(i + 1),
|
||||
];
|
||||
|
||||
try {
|
||||
final repository = await ref.read(submissionsRepositoryProvider.future);
|
||||
if (!ref.mounted) break;
|
||||
|
||||
final fileUrl = await repository.uploadProjectFile(
|
||||
projectName: projectName,
|
||||
filePath: state[i].filePath,
|
||||
);
|
||||
|
||||
if (!ref.mounted) break;
|
||||
|
||||
// Mark as uploaded
|
||||
state = [
|
||||
...state.sublist(0, i),
|
||||
state[i].copyWith(
|
||||
isUploading: false,
|
||||
isUploaded: true,
|
||||
fileUrl: fileUrl,
|
||||
),
|
||||
...state.sublist(i + 1),
|
||||
];
|
||||
|
||||
uploadedUrls.add(fileUrl);
|
||||
} catch (e) {
|
||||
if (!ref.mounted) break;
|
||||
|
||||
// Mark as failed
|
||||
state = [
|
||||
...state.sublist(0, i),
|
||||
state[i].copyWith(
|
||||
isUploading: false,
|
||||
error: e.toString(),
|
||||
),
|
||||
...state.sublist(i + 1),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return uploadedUrls;
|
||||
}
|
||||
|
||||
/// Clear all files
|
||||
void clear() {
|
||||
state = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,62 @@ final class ProjectStatusLocalDataSourceProvider
|
||||
String _$projectStatusLocalDataSourceHash() =>
|
||||
r'c57291e51bd390f9524369860c241d7a0a90fdbf';
|
||||
|
||||
/// Project Progress Local Data Source Provider
|
||||
|
||||
@ProviderFor(projectProgressLocalDataSource)
|
||||
const projectProgressLocalDataSourceProvider =
|
||||
ProjectProgressLocalDataSourceProvider._();
|
||||
|
||||
/// Project Progress Local Data Source Provider
|
||||
|
||||
final class ProjectProgressLocalDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
ProjectProgressLocalDataSource,
|
||||
ProjectProgressLocalDataSource,
|
||||
ProjectProgressLocalDataSource
|
||||
>
|
||||
with $Provider<ProjectProgressLocalDataSource> {
|
||||
/// Project Progress Local Data Source Provider
|
||||
const ProjectProgressLocalDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'projectProgressLocalDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$projectProgressLocalDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<ProjectProgressLocalDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
ProjectProgressLocalDataSource create(Ref ref) {
|
||||
return projectProgressLocalDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ProjectProgressLocalDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ProjectProgressLocalDataSource>(
|
||||
value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$projectProgressLocalDataSourceHash() =>
|
||||
r'653d03b47f5642f3391e7a312649a2603489b224';
|
||||
|
||||
/// Submissions Remote Data Source Provider
|
||||
|
||||
@ProviderFor(submissionsRemoteDataSource)
|
||||
@@ -155,7 +211,7 @@ final class SubmissionsRepositoryProvider
|
||||
}
|
||||
|
||||
String _$submissionsRepositoryHash() =>
|
||||
r'd8261cc538c1fdaa47064e4945302b80f49098bb';
|
||||
r'652208a4ef93cde9b40ae66164d44bba786dfed0';
|
||||
|
||||
/// Project Status List Provider
|
||||
///
|
||||
@@ -221,10 +277,80 @@ abstract class _$ProjectStatusList extends $AsyncNotifier<List<ProjectStatus>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Project Progress List Provider
|
||||
///
|
||||
/// Fetches construction progress stages from API with cache-first pattern.
|
||||
/// Used for dropdown selection when creating/updating project submissions.
|
||||
|
||||
@ProviderFor(ProjectProgressList)
|
||||
const projectProgressListProvider = ProjectProgressListProvider._();
|
||||
|
||||
/// Project Progress List Provider
|
||||
///
|
||||
/// Fetches construction progress stages from API with cache-first pattern.
|
||||
/// Used for dropdown selection when creating/updating project submissions.
|
||||
final class ProjectProgressListProvider
|
||||
extends $AsyncNotifierProvider<ProjectProgressList, List<ProjectProgress>> {
|
||||
/// Project Progress List Provider
|
||||
///
|
||||
/// Fetches construction progress stages from API with cache-first pattern.
|
||||
/// Used for dropdown selection when creating/updating project submissions.
|
||||
const ProjectProgressListProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'projectProgressListProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$projectProgressListHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ProjectProgressList create() => ProjectProgressList();
|
||||
}
|
||||
|
||||
String _$projectProgressListHash() =>
|
||||
r'5ee1c23f90bfa61237f38a6b72c353f0ecb7a2a9';
|
||||
|
||||
/// Project Progress List Provider
|
||||
///
|
||||
/// Fetches construction progress stages from API with cache-first pattern.
|
||||
/// Used for dropdown selection when creating/updating project submissions.
|
||||
|
||||
abstract class _$ProjectProgressList
|
||||
extends $AsyncNotifier<List<ProjectProgress>> {
|
||||
FutureOr<List<ProjectProgress>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<AsyncValue<List<ProjectProgress>>, List<ProjectProgress>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
AsyncValue<List<ProjectProgress>>,
|
||||
List<ProjectProgress>
|
||||
>,
|
||||
AsyncValue<List<ProjectProgress>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// All Submissions Provider
|
||||
///
|
||||
/// Fetches and manages submissions data from remote.
|
||||
/// Waits for project status list to be loaded first.
|
||||
/// Waits for project status list and progress list to be loaded first.
|
||||
|
||||
@ProviderFor(AllSubmissions)
|
||||
const allSubmissionsProvider = AllSubmissionsProvider._();
|
||||
@@ -232,13 +358,13 @@ const allSubmissionsProvider = AllSubmissionsProvider._();
|
||||
/// All Submissions Provider
|
||||
///
|
||||
/// Fetches and manages submissions data from remote.
|
||||
/// Waits for project status list to be loaded first.
|
||||
/// Waits for project status list and progress list to be loaded first.
|
||||
final class AllSubmissionsProvider
|
||||
extends $AsyncNotifierProvider<AllSubmissions, List<ProjectSubmission>> {
|
||||
/// All Submissions Provider
|
||||
///
|
||||
/// Fetches and manages submissions data from remote.
|
||||
/// Waits for project status list to be loaded first.
|
||||
/// Waits for project status list and progress list to be loaded first.
|
||||
const AllSubmissionsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
@@ -258,12 +384,12 @@ final class AllSubmissionsProvider
|
||||
AllSubmissions create() => AllSubmissions();
|
||||
}
|
||||
|
||||
String _$allSubmissionsHash() => r'a4a7fb0d2953efb21e2e6343429f7550c763ea85';
|
||||
String _$allSubmissionsHash() => r'ab0f1ffdc5e6bdb62dbd56ff3e586ecc1ff05bea';
|
||||
|
||||
/// All Submissions Provider
|
||||
///
|
||||
/// Fetches and manages submissions data from remote.
|
||||
/// Waits for project status list to be loaded first.
|
||||
/// Waits for project status list and progress list to be loaded first.
|
||||
|
||||
abstract class _$AllSubmissions
|
||||
extends $AsyncNotifier<List<ProjectSubmission>> {
|
||||
@@ -442,3 +568,142 @@ final class FilteredSubmissionsProvider
|
||||
|
||||
String _$filteredSubmissionsHash() =>
|
||||
r'5be22b3242426c6b0c2f9778eaee5c7cf23e4814';
|
||||
|
||||
/// Save Submission Provider
|
||||
///
|
||||
/// Handles creating new project submissions via API.
|
||||
|
||||
@ProviderFor(SaveSubmission)
|
||||
const saveSubmissionProvider = SaveSubmissionProvider._();
|
||||
|
||||
/// Save Submission Provider
|
||||
///
|
||||
/// Handles creating new project submissions via API.
|
||||
final class SaveSubmissionProvider
|
||||
extends $NotifierProvider<SaveSubmission, AsyncValue<void>> {
|
||||
/// Save Submission Provider
|
||||
///
|
||||
/// Handles creating new project submissions via API.
|
||||
const SaveSubmissionProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'saveSubmissionProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$saveSubmissionHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SaveSubmission create() => SaveSubmission();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AsyncValue<void> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AsyncValue<void>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$saveSubmissionHash() => r'64afa1a9662c36431c143c46a8ca34a786cb0860';
|
||||
|
||||
/// Save Submission Provider
|
||||
///
|
||||
/// Handles creating new project submissions via API.
|
||||
|
||||
abstract class _$SaveSubmission extends $Notifier<AsyncValue<void>> {
|
||||
AsyncValue<void> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<void>, AsyncValue<void>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<void>, AsyncValue<void>>,
|
||||
AsyncValue<void>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload Project Files Provider
|
||||
///
|
||||
/// Handles uploading multiple files for a project submission.
|
||||
/// Tracks upload state for each file individually.
|
||||
|
||||
@ProviderFor(UploadProjectFiles)
|
||||
const uploadProjectFilesProvider = UploadProjectFilesProvider._();
|
||||
|
||||
/// Upload Project Files Provider
|
||||
///
|
||||
/// Handles uploading multiple files for a project submission.
|
||||
/// Tracks upload state for each file individually.
|
||||
final class UploadProjectFilesProvider
|
||||
extends $NotifierProvider<UploadProjectFiles, List<FileUploadState>> {
|
||||
/// Upload Project Files Provider
|
||||
///
|
||||
/// Handles uploading multiple files for a project submission.
|
||||
/// Tracks upload state for each file individually.
|
||||
const UploadProjectFilesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'uploadProjectFilesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$uploadProjectFilesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
UploadProjectFiles create() => UploadProjectFiles();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(List<FileUploadState> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<List<FileUploadState>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$uploadProjectFilesHash() =>
|
||||
r'd6219bc1f0b0d6ac70b9e3cea731267c82a68e1f';
|
||||
|
||||
/// Upload Project Files Provider
|
||||
///
|
||||
/// Handles uploading multiple files for a project submission.
|
||||
/// Tracks upload state for each file individually.
|
||||
|
||||
abstract class _$UploadProjectFiles extends $Notifier<List<FileUploadState>> {
|
||||
List<FileUploadState> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<List<FileUploadState>, List<FileUploadState>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<List<FileUploadState>, List<FileUploadState>>,
|
||||
List<FileUploadState>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user