Files
worker/lib/features/projects/presentation/pages/submissions_page.dart
Phuoc Nguyen ba04576750 add
2025-11-27 14:59:48 +07:00

383 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: () => context.push(RouteNames.submissionCreate),
),
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, 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, 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}')),
);
},
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}',
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;
}
}
}