Files
worker/lib/features/showrooms/presentation/pages/model_houses_page.dart
Phuoc Nguyen 49a41d24eb update theme
2025-12-02 15:20:54 +07:00

589 lines
19 KiB
Dart

/// Page: Model Houses Page
///
/// Displays model house library and design requests following html/nha-mau.html.
library;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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/showrooms/domain/entities/design_request.dart';
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
import 'package:worker/features/showrooms/presentation/providers/design_request_provider.dart';
import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart';
/// Model Houses Page
///
/// Two tabs:
/// 1. Thư viện mẫu - Model house library with 360° views
/// 2. Yêu cầu thiết kế - Design requests with status tracking
class ModelHousesPage extends ConsumerStatefulWidget {
const ModelHousesPage({super.key});
@override
ConsumerState<ModelHousesPage> createState() => _ModelHousesPageState();
}
class _ModelHousesPageState extends ConsumerState<ModelHousesPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
void _showInfoDialog() {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text(
'Hướng dẫn sử dụng',
style: TextStyle(fontWeight: FontWeight.bold),
),
content: const SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Đây là nội dung hướng dẫn sử dụng cho tính năng Nhà mẫu:'),
SizedBox(height: 12),
Text(
'• Tab "Thư viện Mẫu 360": Là nơi công ty cung cấp các mẫu thiết kế 360° có sẵn để bạn tham khảo.',
),
SizedBox(height: 8),
Text(
'• Tab "Yêu cầu Thiết kế": Là nơi bạn gửi yêu cầu (ticket) để đội ngũ thiết kế của chúng tôi hỗ trợ bạn.',
),
SizedBox(height: 8),
Text(
'• Bấm nút "+" trong tab "Yêu cầu Thiết kế" để tạo một Yêu cầu Thiết kế mới.',
),
SizedBox(height: 8),
Text(
'• Khi yêu cầu hoàn thành, bạn có thể xem link thiết kế 3D trong trang chi tiết yêu cầu.',
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Đóng'),
),
],
),
);
}
void _createNewRequest() {
context.push(RouteNames.designRequestCreate);
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface),
onPressed: () => Navigator.of(context).pop(),
),
centerTitle: false,
title: Text(
'Nhà mẫu',
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
actions: [
IconButton(
icon: Icon(Icons.info_outline, color: colorScheme.onSurface),
onPressed: _showInfoDialog,
),
const SizedBox(width: AppSpacing.sm),
],
bottom: TabBar(
controller: _tabController,
indicatorColor: colorScheme.primary,
indicatorWeight: 3,
labelColor: colorScheme.primary,
labelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
unselectedLabelColor: colorScheme.onSurfaceVariant,
unselectedLabelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
tabs: const [
Tab(text: 'Thư viện mẫu'),
Tab(text: 'Yêu cầu thiết kế'),
],
),
),
body: TabBarView(
controller: _tabController,
children: const [_LibraryTab(), _DesignRequestsTab()],
),
floatingActionButton: AnimatedBuilder(
animation: _tabController,
builder: (context, child) {
// Show FAB only on Design Requests tab
return _tabController.index == 1
? FloatingActionButton(
onPressed: _createNewRequest,
backgroundColor: colorScheme.primary,
elevation: 4,
child: Icon(
Icons.add,
color: colorScheme.onPrimary,
size: 28,
),
)
: const SizedBox.shrink();
},
),
);
}
}
/// Library Tab - Model house 360° library
class _LibraryTab extends ConsumerWidget {
const _LibraryTab();
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final sampleProjectsAsync = ref.watch(sampleProjectsListProvider);
return sampleProjectsAsync.when(
data: (projects) {
if (projects.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.home_work_outlined,
size: 64,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'Chưa có mẫu nhà nào',
style: TextStyle(
fontSize: 16,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
return RefreshIndicator(
onRefresh: () => ref.read(sampleProjectsListProvider.notifier).refresh(),
child: ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: projects.length,
itemBuilder: (context, index) {
final project = projects[index];
return _LibraryCard(project: project);
},
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stack) => Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: AppColors.danger,
),
const SizedBox(height: 16),
Text(
'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.invalidate(sampleProjectsListProvider),
child: const Text('Thử lại'),
),
],
),
),
),
);
}
}
/// Library Card Widget
class _LibraryCard extends StatelessWidget {
const _LibraryCard({required this.project});
final SampleProject project;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.only(bottom: 20),
child: InkWell(
onTap: () {
context.push('/model-houses/${project.id}');
},
borderRadius: BorderRadius.circular(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image with 360 badge
Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: project.thumbnailUrl != null
? CachedNetworkImage(
imageUrl: project.thumbnailUrl!,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
height: 200,
color: colorScheme.surfaceContainerHighest,
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => Container(
height: 200,
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.image_not_supported,
size: 48,
color: colorScheme.onSurfaceVariant,
),
),
)
: Container(
height: 200,
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.home_work,
size: 48,
color: colorScheme.onSurfaceVariant,
),
),
),
if (project.has360View)
Positioned(
top: 12,
right: 12,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Xem 360°',
style: TextStyle(
color: colorScheme.onPrimary,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
// Content
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Text(
project.projectName,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
if (project.plainDescription.isNotEmpty) ...[
const SizedBox(height: 12),
// Description
Text(
project.plainDescription,
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
height: 1.5,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
),
);
}
}
/// Design Requests Tab
class _DesignRequestsTab extends ConsumerWidget {
const _DesignRequestsTab();
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final requestsAsync = ref.watch(designRequestsListProvider);
return requestsAsync.when(
data: (requests) {
if (requests.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.design_services_outlined,
size: 64,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'Chưa có yêu cầu thiết kế nào',
style: TextStyle(
fontSize: 16,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
return RefreshIndicator(
onRefresh: () => ref.read(designRequestsListProvider.notifier).refresh(),
child: ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: requests.length,
itemBuilder: (context, index) {
final request = requests[index];
return _RequestCard(request: request);
},
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stack) => Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: AppColors.danger,
),
const SizedBox(height: 16),
Text(
'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.invalidate(designRequestsListProvider),
child: const Text('Thử lại'),
),
],
),
),
),
);
}
}
/// Request Card Widget
class _RequestCard extends StatelessWidget {
const _RequestCard({required this.request});
final DesignRequest request;
Color _getStatusColor() {
switch (request.status) {
case DesignRequestStatus.pending:
return const Color(0xFFffc107); // Warning yellow
case DesignRequestStatus.designing:
return const Color(0xFF3730a3); // Indigo
case DesignRequestStatus.completed:
return const Color(0xFF065f46); // Success green
case DesignRequestStatus.rejected:
return const Color(0xFFdc2626); // Danger red
}
}
Color _getStatusBackgroundColor() {
switch (request.status) {
case DesignRequestStatus.pending:
return const Color(0xFFfef3c7); // Light yellow
case DesignRequestStatus.designing:
return const Color(0xFFe0e7ff); // Light indigo
case DesignRequestStatus.completed:
return const Color(0xFFd1fae5); // Light green
case DesignRequestStatus.rejected:
return const Color(0xFFfee2e2); // Light red
}
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () {
context.push('/model-houses/design-request/${request.id}');
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: Code and Status
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Mã yêu cầu: #${request.id}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: _getStatusBackgroundColor(),
borderRadius: BorderRadius.circular(20),
),
child: Text(
request.statusText.toUpperCase(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _getStatusColor(),
),
),
),
],
),
const SizedBox(height: 8),
// Date
if (request.dateline != null)
Text(
'Deadline: ${request.dateline}',
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
),
const SizedBox(height: 8),
// Subject
Text(
request.subject,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
if (request.plainDescription.isNotEmpty) ...[
const SizedBox(height: 4),
// Description
Text(
request.plainDescription,
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
),
);
}
}