394 lines
14 KiB
Dart
394 lines
14 KiB
Dart
/// Page: Project Submissions List
|
|
///
|
|
/// Displays list of user's project submissions with filters.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:intl/intl.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/projects/domain/entities/project_submission.dart';
|
|
import 'package:worker/features/projects/presentation/providers/submissions_provider.dart';
|
|
|
|
/// Project Submissions Page
|
|
class SubmissionsPage extends ConsumerWidget {
|
|
const SubmissionsPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final submissionsAsync = ref.watch(filteredSubmissionsProvider);
|
|
final statusListAsync = ref.watch(projectStatusListProvider);
|
|
final filter = ref.watch(submissionsFilterProvider);
|
|
final selectedStatus = filter.selectedStatus;
|
|
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF4F6F8),
|
|
appBar: AppBar(
|
|
leading: IconButton(
|
|
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
title: const Text(
|
|
'Danh sách Dự án',
|
|
style: TextStyle(color: Colors.black),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: const FaIcon(FontAwesomeIcons.plus, color: Colors.black, size: 20),
|
|
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),
|
|
],
|
|
elevation: AppBarSpecs.elevation,
|
|
backgroundColor: AppColors.white,
|
|
centerTitle: false,
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// Search Bar
|
|
Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: TextField(
|
|
decoration: InputDecoration(
|
|
hintText: 'Mã dự án hoặc tên công trình',
|
|
prefixIcon: const Icon(Icons.search, color: AppColors.grey500),
|
|
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),
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
ref.read(submissionsFilterProvider.notifier).updateSearchQuery(value);
|
|
},
|
|
),
|
|
),
|
|
|
|
// Status Filter Tabs
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
children: [
|
|
_buildFilterChip(
|
|
context,
|
|
ref,
|
|
label: 'Tất cả',
|
|
isSelected: selectedStatus == null,
|
|
onTap: () => ref.read(submissionsFilterProvider.notifier).clearStatusFilter(),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Use projectStatusListProvider to get status options
|
|
statusListAsync.when(
|
|
data: (statuses) => Row(
|
|
children: statuses.map((status) => Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: _buildFilterChip(
|
|
context,
|
|
ref,
|
|
label: status.label,
|
|
isSelected: selectedStatus == status.label,
|
|
onTap: () => ref.read(submissionsFilterProvider.notifier).selectStatus(status.label),
|
|
),
|
|
)).toList(),
|
|
),
|
|
loading: () => const SizedBox.shrink(),
|
|
error: (_, __) => const SizedBox.shrink(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Submissions List
|
|
Expanded(
|
|
child: submissionsAsync.when(
|
|
data: (submissions) {
|
|
if (submissions.isEmpty) {
|
|
return RefreshIndicator(
|
|
onRefresh: () async {
|
|
await ref.read(allSubmissionsProvider.notifier).refresh();
|
|
},
|
|
child: ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
children: [
|
|
SizedBox(
|
|
height: MediaQuery.of(context).size.height * 0.5,
|
|
child: const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
FaIcon(
|
|
FontAwesomeIcons.folderOpen,
|
|
size: 64,
|
|
color: AppColors.grey500,
|
|
),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'Không có dự án nào',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.grey900,
|
|
),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'Không tìm thấy dự án phù hợp',
|
|
style: TextStyle(color: AppColors.grey500),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: () async {
|
|
await ref.read(allSubmissionsProvider.notifier).refresh();
|
|
},
|
|
child: ListView.builder(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
itemCount: submissions.length,
|
|
itemBuilder: (context, index) {
|
|
final submission = submissions[index];
|
|
return _buildSubmissionCard(context, ref, submission);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
loading: () => const Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
error: (error, stack) => RefreshIndicator(
|
|
onRefresh: () async {
|
|
await ref.read(allSubmissionsProvider.notifier).refresh();
|
|
},
|
|
child: ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
children: [
|
|
SizedBox(
|
|
height: MediaQuery.of(context).size.height * 0.5,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(
|
|
Icons.error_outline,
|
|
size: 64,
|
|
color: AppColors.danger,
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Có lỗi xảy ra',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.grey900,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
error.toString(),
|
|
style: const TextStyle(color: AppColors.grey500),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Kéo xuống để thử lại',
|
|
style: TextStyle(color: AppColors.grey500),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFilterChip(
|
|
BuildContext context,
|
|
WidgetRef ref, {
|
|
required String label,
|
|
required bool isSelected,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? AppColors.primaryBlue : AppColors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: isSelected ? AppColors.primaryBlue : AppColors.grey100,
|
|
),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: isSelected ? AppColors.white : AppColors.grey900,
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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: () 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(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
submission.designedArea,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.grey900,
|
|
),
|
|
),
|
|
_buildStatusBadge(submission.status, submission.statusColor),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Text(
|
|
// 'Tên công trình: ${submission.designedArea}',
|
|
// style: const TextStyle(
|
|
// fontSize: 14,
|
|
// color: AppColors.grey900,
|
|
// ),
|
|
// ),
|
|
// const SizedBox(height: 4),
|
|
Text(
|
|
'Ngày nộp: ${DateFormat('dd/MM/yyyy HH:mm').format(submission.requestDate)}',
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
color: AppColors.grey900,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Diện tích: ${submission.designArea} m²',
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: AppColors.grey900,
|
|
),
|
|
),
|
|
if (submission.reasonForRejection != null) ...[
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFEF2F2),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const FaIcon(
|
|
FontAwesomeIcons.triangleExclamation,
|
|
size: 14,
|
|
color: AppColors.danger,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
submission.reasonForRejection!,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: AppColors.danger,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusBadge(String status, String statusColor) {
|
|
final color = _getColorFromStatusColor(statusColor);
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
status,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Color _getColorFromStatusColor(String statusColor) {
|
|
switch (statusColor) {
|
|
case 'Warning':
|
|
return AppColors.warning;
|
|
case 'Success':
|
|
return AppColors.success;
|
|
case 'Danger':
|
|
return AppColors.danger;
|
|
case 'Info':
|
|
return AppColors.info;
|
|
default:
|
|
return AppColors.grey500;
|
|
}
|
|
}
|
|
}
|