point record

This commit is contained in:
Phuoc Nguyen
2025-11-26 11:48:02 +07:00
parent 3741239d83
commit a07f165f0c
12 changed files with 1761 additions and 12 deletions

View File

@@ -27,6 +27,7 @@ import 'package:worker/features/chat/presentation/pages/chat_list_page.dart';
import 'package:worker/features/favorites/presentation/pages/favorites_page.dart'; import 'package:worker/features/favorites/presentation/pages/favorites_page.dart';
import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart'; import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart';
import 'package:worker/features/loyalty/presentation/pages/points_history_page.dart'; import 'package:worker/features/loyalty/presentation/pages/points_history_page.dart';
import 'package:worker/features/loyalty/presentation/pages/points_records_page.dart';
import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart'; import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart';
import 'package:worker/features/main/presentation/pages/main_scaffold.dart'; import 'package:worker/features/main/presentation/pages/main_scaffold.dart';
import 'package:worker/features/news/presentation/pages/news_detail_page.dart'; import 'package:worker/features/news/presentation/pages/news_detail_page.dart';
@@ -47,6 +48,7 @@ import 'package:worker/features/promotions/presentation/pages/promotion_detail_p
import 'package:worker/features/quotes/presentation/pages/quotes_page.dart'; import 'package:worker/features/quotes/presentation/pages/quotes_page.dart';
import 'package:worker/features/showrooms/presentation/pages/design_request_create_page.dart'; import 'package:worker/features/showrooms/presentation/pages/design_request_create_page.dart';
import 'package:worker/features/showrooms/presentation/pages/design_request_detail_page.dart'; import 'package:worker/features/showrooms/presentation/pages/design_request_detail_page.dart';
import 'package:worker/features/showrooms/presentation/pages/model_house_detail_page.dart';
import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart'; import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart';
/// Router Provider /// Router Provider
@@ -273,6 +275,14 @@ final routerProvider = Provider<GoRouter>((ref) {
MaterialPage(key: state.pageKey, child: const PointsHistoryPage()), MaterialPage(key: state.pageKey, child: const PointsHistoryPage()),
), ),
// Points Records Route
GoRoute(
path: RouteNames.pointsRecords,
name: 'loyalty_points_records',
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const PointsRecordsPage()),
),
// Orders Route // Orders Route
GoRoute( GoRoute(
path: RouteNames.orders, path: RouteNames.orders,
@@ -467,6 +477,19 @@ final routerProvider = Provider<GoRouter>((ref) {
MaterialPage(key: state.pageKey, child: const ModelHousesPage()), MaterialPage(key: state.pageKey, child: const ModelHousesPage()),
), ),
// Model House Detail Route
GoRoute(
path: RouteNames.modelHouseDetail,
name: RouteNames.modelHouseDetail,
pageBuilder: (context, state) {
final modelId = state.pathParameters['id'];
return MaterialPage(
key: state.pageKey,
child: ModelHouseDetailPage(modelId: modelId ?? ''),
);
},
),
// Design Request Create Route // Design Request Create Route
GoRoute( GoRoute(
path: RouteNames.designRequestCreate, path: RouteNames.designRequestCreate,
@@ -558,6 +581,7 @@ class RouteNames {
static const String loyalty = '/loyalty'; static const String loyalty = '/loyalty';
static const String rewards = '/loyalty/rewards'; static const String rewards = '/loyalty/rewards';
static const String pointsHistory = '/loyalty/points-history'; static const String pointsHistory = '/loyalty/points-history';
static const String pointsRecords = '/$loyalty/points-records';
static const String myGifts = '/loyalty/gifts'; static const String myGifts = '/loyalty/gifts';
static const String referral = '/loyalty/referral'; static const String referral = '/loyalty/referral';
@@ -603,6 +627,7 @@ class RouteNames {
// Model Houses & Design Requests Routes // Model Houses & Design Requests Routes
static const String modelHouses = '/model-houses'; static const String modelHouses = '/model-houses';
static const String modelHouseDetail = '/model-houses/:id';
static const String designRequestCreate = static const String designRequestCreate =
'/model-houses/design-request/create'; '/model-houses/design-request/create';
static const String designRequestDetail = '/model-houses/design-request/:id'; static const String designRequestDetail = '/model-houses/design-request/:id';

View File

@@ -197,8 +197,7 @@ class _HomePageState extends ConsumerState<HomePage> {
QuickAction( QuickAction(
icon: FontAwesomeIcons.circlePlus, icon: FontAwesomeIcons.circlePlus,
label: 'Ghi nhận điểm', label: 'Ghi nhận điểm',
onTap: () => onTap: () => context.push(RouteNames.pointsRecords),
_showComingSoon(context, 'Ghi nhận điểm', l10n),
), ),
QuickAction( QuickAction(
icon: FontAwesomeIcons.gift, icon: FontAwesomeIcons.gift,

View File

@@ -0,0 +1,136 @@
/// Remote Data Source: Points Record
///
/// Handles API communication for points records.
library;
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
/// Points Record Remote Data Source Interface
abstract class PointsRecordRemoteDataSource {
/// Get all points records for current user
Future<List<PointsRecord>> getPointsRecords();
/// Get single points record by ID
Future<PointsRecord> getPointsRecordById(String recordId);
/// Submit new points record
Future<PointsRecord> submitPointsRecord(PointsRecord record);
}
/// Points Record Remote Data Source Implementation (Mock)
class PointsRecordRemoteDataSourceImpl
implements PointsRecordRemoteDataSource {
@override
Future<List<PointsRecord>> getPointsRecords() async {
await Future<void>.delayed(const Duration(milliseconds: 500));
return [
PointsRecord(
recordId: 'PRR001',
userId: 'user123',
invoiceNumber: 'INV-VG-001',
storeName: 'Công ty TNHH Vingroup',
transactionDate: DateTime(2023, 11, 15),
invoiceAmount: 2500000,
notes: 'Gạch granite cao cấp cho khu vực lobby và hành lang',
attachments: const [
'https://example.com/invoice1.jpg',
'https://example.com/invoice2.jpg',
],
status: PointsStatus.approved,
pointsEarned: 250,
submittedAt: DateTime(2023, 11, 15, 10, 0),
processedAt: DateTime(2023, 11, 20, 14, 30),
processedBy: 'admin001',
),
PointsRecord(
recordId: 'PRR002',
userId: 'user123',
invoiceNumber: 'INV-BTX-002',
storeName: 'Tập đoàn Bitexco',
transactionDate: DateTime(2023, 11, 25),
invoiceAmount: 1250000,
notes: 'Gạch porcelain 80x80 cho sảnh chính và khu mua sắm',
attachments: const [
'https://example.com/invoice3.jpg',
],
status: PointsStatus.pending,
submittedAt: DateTime(2023, 11, 25, 9, 15),
),
PointsRecord(
recordId: 'PRR003',
userId: 'user123',
invoiceNumber: 'INV-ABC-003',
storeName: 'Công ty TNHH ABC Manufacturing',
transactionDate: DateTime(2023, 11, 20),
invoiceAmount: 4200000,
notes: 'Gạch chống trơn cho khu vực sản xuất và kho bãi',
attachments: const [
'https://example.com/invoice4.jpg',
'https://example.com/invoice5.jpg',
],
status: PointsStatus.rejected,
rejectReason: 'Hình ảnh minh chứng không hợp lệ',
submittedAt: DateTime(2023, 11, 20, 11, 0),
processedAt: DateTime(2023, 11, 28, 16, 45),
processedBy: 'admin002',
),
PointsRecord(
recordId: 'PRR004',
userId: 'user123',
invoiceNumber: 'INV-ECO-004',
storeName: 'Ecopark Group',
transactionDate: DateTime(2023, 10, 10),
invoiceAmount: 3700000,
notes: 'Gạch ceramic vân gỗ cho khu vực phòng khách và sân vườn',
attachments: const [
'https://example.com/invoice6.jpg',
],
status: PointsStatus.approved,
pointsEarned: 370,
submittedAt: DateTime(2023, 10, 10, 8, 30),
processedAt: DateTime(2023, 10, 15, 10, 20),
processedBy: 'admin001',
),
PointsRecord(
recordId: 'PRR005',
userId: 'user123',
invoiceNumber: 'INV-DMD-005',
storeName: 'Diamond Hospitality Group',
transactionDate: DateTime(2023, 12, 1),
invoiceAmount: 8600000,
notes: 'Gạch marble tự nhiên cho lobby và phòng suite',
attachments: const [
'https://example.com/invoice7.jpg',
'https://example.com/invoice8.jpg',
'https://example.com/invoice9.jpg',
],
status: PointsStatus.pending,
submittedAt: DateTime(2023, 12, 1, 13, 0),
),
];
}
@override
Future<PointsRecord> getPointsRecordById(String recordId) async {
await Future<void>.delayed(const Duration(milliseconds: 300));
final records = await getPointsRecords();
return records.firstWhere(
(record) => record.recordId == recordId,
orElse: () => throw Exception('Points record not found'),
);
}
@override
Future<PointsRecord> submitPointsRecord(PointsRecord record) async {
await Future<void>.delayed(const Duration(milliseconds: 800));
// Simulate successful submission
return record.copyWith(
recordId: 'PRR${DateTime.now().millisecondsSinceEpoch}',
status: PointsStatus.pending,
submittedAt: DateTime.now(),
);
}
}

View File

@@ -0,0 +1,42 @@
/// Repository Implementation: Points Record
///
/// Implements points record repository interface.
library;
import 'package:worker/features/loyalty/data/datasources/points_record_remote_datasource.dart';
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
import 'package:worker/features/loyalty/domain/repositories/points_record_repository.dart';
/// Points Record Repository Implementation
class PointsRecordRepositoryImpl implements PointsRecordRepository {
const PointsRecordRepositoryImpl(this._remoteDataSource);
final PointsRecordRemoteDataSource _remoteDataSource;
@override
Future<List<PointsRecord>> getPointsRecords() async {
try {
return await _remoteDataSource.getPointsRecords();
} catch (e) {
rethrow;
}
}
@override
Future<PointsRecord> getPointsRecordById(String recordId) async {
try {
return await _remoteDataSource.getPointsRecordById(recordId);
} catch (e) {
rethrow;
}
}
@override
Future<PointsRecord> submitPointsRecord(PointsRecord record) async {
try {
return await _remoteDataSource.submitPointsRecord(record);
} catch (e) {
rethrow;
}
}
}

View File

@@ -18,11 +18,11 @@ enum PointsStatus {
String get displayName { String get displayName {
switch (this) { switch (this) {
case PointsStatus.pending: case PointsStatus.pending:
return 'Pending'; return 'Chờ duyệt';
case PointsStatus.approved: case PointsStatus.approved:
return 'Approved'; return 'Đã duyệt';
case PointsStatus.rejected: case PointsStatus.rejected:
return 'Rejected'; return 'Bị từ chối';
} }
} }
} }

View File

@@ -0,0 +1,18 @@
/// Repository Interface: Points Record
///
/// Defines contract for points record operations.
library;
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
/// Points Record Repository Interface
abstract class PointsRecordRepository {
/// Get all points records for current user
Future<List<PointsRecord>> getPointsRecords();
/// Get single points record by ID
Future<PointsRecord> getPointsRecordById(String recordId);
/// Submit new points record
Future<PointsRecord> submitPointsRecord(PointsRecord record);
}

View File

@@ -0,0 +1,19 @@
/// Use Case: Get Points Records
///
/// Retrieves all points records for the current user.
library;
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
import 'package:worker/features/loyalty/domain/repositories/points_record_repository.dart';
/// Get Points Records Use Case
class GetPointsRecords {
const GetPointsRecords(this._repository);
final PointsRecordRepository _repository;
/// Execute use case
Future<List<PointsRecord>> call() async {
return await _repository.getPointsRecords();
}
}

View File

@@ -0,0 +1,386 @@
/// Page: Points Records List
///
/// Displays list of user's points records 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/theme/colors.dart';
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
import 'package:worker/features/loyalty/presentation/providers/points_records_provider.dart';
/// Points Records Page
class PointsRecordsPage extends ConsumerWidget {
const PointsRecordsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final recordsAsync = ref.watch(filteredPointsRecordsProvider);
final filter = ref.watch(pointsRecordsFilterProvider);
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: () => context.pop(),
),
title: const Text(
'Danh sách Ghi nhận điểm',
style: TextStyle(color: Colors.black),
),
actions: [
IconButton(
icon: const FaIcon(
FontAwesomeIcons.plus,
color: Colors.black,
size: 20,
),
onPressed: () {
// TODO: Navigate to points record create page
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Tính năng tạo ghi nhận điểm sẽ được cập nhật'),
),
);
},
),
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ã yêu cầu',
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(pointsRecordsFilterProvider.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(pointsRecordsFilterProvider.notifier).clearStatusFilter(),
),
const SizedBox(width: 8),
...PointsStatus.values.map(
(status) => Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildFilterChip(
context,
ref,
label: status.displayName,
isSelected: selectedStatus == status,
onTap: () =>
ref.read(pointsRecordsFilterProvider.notifier).selectStatus(status),
),
),
),
],
),
),
const SizedBox(height: 16),
// Points Records List
Expanded(
child: recordsAsync.when(
data: (records) {
if (records.isEmpty) {
return RefreshIndicator(
onRefresh: () async {
await ref.read(allPointsRecordsProvider.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ó ghi nhận điểm nào',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
SizedBox(height: 8),
Text(
'Không tìm thấy ghi nhận điểm phù hợp',
style: TextStyle(color: AppColors.grey500),
),
],
),
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () async {
await ref.read(allPointsRecordsProvider.notifier).refresh();
},
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: records.length,
itemBuilder: (context, index) {
final record = records[index];
return _buildRecordCard(context, record);
},
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stack) => RefreshIndicator(
onRefresh: () async {
await ref.read(allPointsRecordsProvider.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 _buildRecordCard(BuildContext context, PointsRecord record) {
final currencyFormat = NumberFormat.currency(
locale: 'vi_VN',
symbol: '',
decimalDigits: 0,
);
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: InkWell(
onTap: () {
// TODO: Navigate to points record detail
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Chi tiết ghi nhận ${record.recordId}')),
);
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'#${record.recordId}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
_buildStatusBadge(record.status),
],
),
const SizedBox(height: 8),
Text(
'Ngày gửi: ${DateFormat('dd/MM/yyyy').format(record.submittedAt)}',
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
),
const SizedBox(height: 4),
Text(
'Giá trị đơn hàng: ${currencyFormat.format(record.invoiceAmount)}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey900,
),
),
if (record.rejectReason != 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(
record.rejectReason!,
style: const TextStyle(
fontSize: 12,
color: AppColors.danger,
),
),
),
],
),
),
],
],
),
),
),
);
}
Widget _buildStatusBadge(PointsStatus status) {
final color = _getStatusColor(status);
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.displayName,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}
Color _getStatusColor(PointsStatus status) {
switch (status) {
case PointsStatus.pending:
return AppColors.warning;
case PointsStatus.approved:
return AppColors.success;
case PointsStatus.rejected:
return AppColors.danger;
}
}
}

View File

@@ -0,0 +1,118 @@
/// Providers: Points Records
///
/// State management for points records feature.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/loyalty/data/datasources/points_record_remote_datasource.dart';
import 'package:worker/features/loyalty/data/repositories/points_record_repository_impl.dart';
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
import 'package:worker/features/loyalty/domain/repositories/points_record_repository.dart';
import 'package:worker/features/loyalty/domain/usecases/get_points_records.dart';
part 'points_records_provider.g.dart';
// ============================================================================
// Data Layer Providers
// ============================================================================
/// Points Record Remote Data Source Provider
@riverpod
PointsRecordRemoteDataSource pointsRecordRemoteDataSource(Ref ref) {
return PointsRecordRemoteDataSourceImpl();
}
/// Points Record Repository Provider
@riverpod
PointsRecordRepository pointsRecordRepository(Ref ref) {
final remoteDataSource = ref.watch(pointsRecordRemoteDataSourceProvider);
return PointsRecordRepositoryImpl(remoteDataSource);
}
/// Get Points Records Use Case Provider
@riverpod
GetPointsRecords getPointsRecords(Ref ref) {
final repository = ref.watch(pointsRecordRepositoryProvider);
return GetPointsRecords(repository);
}
// ============================================================================
// Presentation Layer Providers
// ============================================================================
/// All Points Records Provider (AsyncNotifier)
@riverpod
class AllPointsRecords extends _$AllPointsRecords {
@override
Future<List<PointsRecord>> build() async {
final useCase = ref.watch(getPointsRecordsProvider);
return await useCase();
}
/// Refresh points records
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final useCase = ref.read(getPointsRecordsProvider);
return await useCase();
});
}
}
/// Points Records Filter State Provider
@riverpod
class PointsRecordsFilter extends _$PointsRecordsFilter {
@override
({String searchQuery, PointsStatus? selectedStatus}) build() {
return (searchQuery: '', selectedStatus: null);
}
/// Update search query
void updateSearchQuery(String query) {
state = (searchQuery: query, selectedStatus: state.selectedStatus);
}
/// Select status filter
void selectStatus(PointsStatus? status) {
state = (searchQuery: state.searchQuery, selectedStatus: status);
}
/// Clear status filter
void clearStatusFilter() {
state = (searchQuery: state.searchQuery, selectedStatus: null);
}
}
/// Filtered Points Records Provider
@riverpod
AsyncValue<List<PointsRecord>> filteredPointsRecords(Ref ref) {
final dataAsync = ref.watch(allPointsRecordsProvider);
final filter = ref.watch(pointsRecordsFilterProvider);
return dataAsync.whenData((records) {
var filtered = records;
// Apply status filter
if (filter.selectedStatus != null) {
filtered = filtered
.where((record) => record.status == filter.selectedStatus)
.toList();
}
// Apply search filter
if (filter.searchQuery.isNotEmpty) {
final query = filter.searchQuery.toLowerCase();
filtered = filtered.where((record) {
final idMatch = record.recordId.toLowerCase().contains(query);
final invoiceMatch = record.invoiceNumber.toLowerCase().contains(query);
final storeMatch = record.storeName.toLowerCase().contains(query);
return idMatch || invoiceMatch || storeMatch;
}).toList();
}
// Sort by submission date (newest first)
filtered.sort((a, b) => b.submittedAt.compareTo(a.submittedAt));
return filtered;
});
}

View File

@@ -0,0 +1,352 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'points_records_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Points Record Remote Data Source Provider
@ProviderFor(pointsRecordRemoteDataSource)
const pointsRecordRemoteDataSourceProvider =
PointsRecordRemoteDataSourceProvider._();
/// Points Record Remote Data Source Provider
final class PointsRecordRemoteDataSourceProvider
extends
$FunctionalProvider<
PointsRecordRemoteDataSource,
PointsRecordRemoteDataSource,
PointsRecordRemoteDataSource
>
with $Provider<PointsRecordRemoteDataSource> {
/// Points Record Remote Data Source Provider
const PointsRecordRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pointsRecordRemoteDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pointsRecordRemoteDataSourceHash();
@$internal
@override
$ProviderElement<PointsRecordRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
PointsRecordRemoteDataSource create(Ref ref) {
return pointsRecordRemoteDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(PointsRecordRemoteDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<PointsRecordRemoteDataSource>(value),
);
}
}
String _$pointsRecordRemoteDataSourceHash() =>
r'b0a68a4bb8f05281afa84885a916611c131e172a';
/// Points Record Repository Provider
@ProviderFor(pointsRecordRepository)
const pointsRecordRepositoryProvider = PointsRecordRepositoryProvider._();
/// Points Record Repository Provider
final class PointsRecordRepositoryProvider
extends
$FunctionalProvider<
PointsRecordRepository,
PointsRecordRepository,
PointsRecordRepository
>
with $Provider<PointsRecordRepository> {
/// Points Record Repository Provider
const PointsRecordRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pointsRecordRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pointsRecordRepositoryHash();
@$internal
@override
$ProviderElement<PointsRecordRepository> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
PointsRecordRepository create(Ref ref) {
return pointsRecordRepository(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(PointsRecordRepository value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<PointsRecordRepository>(value),
);
}
}
String _$pointsRecordRepositoryHash() =>
r'892ad8ca77bd5c59dba2e285163ef5e016c0ca0d';
/// Get Points Records Use Case Provider
@ProviderFor(getPointsRecords)
const getPointsRecordsProvider = GetPointsRecordsProvider._();
/// Get Points Records Use Case Provider
final class GetPointsRecordsProvider
extends
$FunctionalProvider<
GetPointsRecords,
GetPointsRecords,
GetPointsRecords
>
with $Provider<GetPointsRecords> {
/// Get Points Records Use Case Provider
const GetPointsRecordsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'getPointsRecordsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$getPointsRecordsHash();
@$internal
@override
$ProviderElement<GetPointsRecords> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
GetPointsRecords create(Ref ref) {
return getPointsRecords(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GetPointsRecords value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GetPointsRecords>(value),
);
}
}
String _$getPointsRecordsHash() => r'f7ea3c5c9675878967cc34b18416adb3a665ccf8';
/// All Points Records Provider (AsyncNotifier)
@ProviderFor(AllPointsRecords)
const allPointsRecordsProvider = AllPointsRecordsProvider._();
/// All Points Records Provider (AsyncNotifier)
final class AllPointsRecordsProvider
extends $AsyncNotifierProvider<AllPointsRecords, List<PointsRecord>> {
/// All Points Records Provider (AsyncNotifier)
const AllPointsRecordsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'allPointsRecordsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$allPointsRecordsHash();
@$internal
@override
AllPointsRecords create() => AllPointsRecords();
}
String _$allPointsRecordsHash() => r'cd64b6952f9abfe1142773b4b88a051b74e8d763';
/// All Points Records Provider (AsyncNotifier)
abstract class _$AllPointsRecords extends $AsyncNotifier<List<PointsRecord>> {
FutureOr<List<PointsRecord>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref as $Ref<AsyncValue<List<PointsRecord>>, List<PointsRecord>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<PointsRecord>>, List<PointsRecord>>,
AsyncValue<List<PointsRecord>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Points Records Filter State Provider
@ProviderFor(PointsRecordsFilter)
const pointsRecordsFilterProvider = PointsRecordsFilterProvider._();
/// Points Records Filter State Provider
final class PointsRecordsFilterProvider
extends
$NotifierProvider<
PointsRecordsFilter,
({String searchQuery, PointsStatus? selectedStatus})
> {
/// Points Records Filter State Provider
const PointsRecordsFilterProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pointsRecordsFilterProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pointsRecordsFilterHash();
@$internal
@override
PointsRecordsFilter create() => PointsRecordsFilter();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(
({String searchQuery, PointsStatus? selectedStatus}) value,
) {
return $ProviderOverride(
origin: this,
providerOverride:
$SyncValueProvider<
({String searchQuery, PointsStatus? selectedStatus})
>(value),
);
}
}
String _$pointsRecordsFilterHash() =>
r'ae81040dff096894330a2a744959190545435c48';
/// Points Records Filter State Provider
abstract class _$PointsRecordsFilter
extends $Notifier<({String searchQuery, PointsStatus? selectedStatus})> {
({String searchQuery, PointsStatus? selectedStatus}) build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref
as $Ref<
({String searchQuery, PointsStatus? selectedStatus}),
({String searchQuery, PointsStatus? selectedStatus})
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
({String searchQuery, PointsStatus? selectedStatus}),
({String searchQuery, PointsStatus? selectedStatus})
>,
({String searchQuery, PointsStatus? selectedStatus}),
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Filtered Points Records Provider
@ProviderFor(filteredPointsRecords)
const filteredPointsRecordsProvider = FilteredPointsRecordsProvider._();
/// Filtered Points Records Provider
final class FilteredPointsRecordsProvider
extends
$FunctionalProvider<
AsyncValue<List<PointsRecord>>,
AsyncValue<List<PointsRecord>>,
AsyncValue<List<PointsRecord>>
>
with $Provider<AsyncValue<List<PointsRecord>>> {
/// Filtered Points Records Provider
const FilteredPointsRecordsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'filteredPointsRecordsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$filteredPointsRecordsHash();
@$internal
@override
$ProviderElement<AsyncValue<List<PointsRecord>>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
AsyncValue<List<PointsRecord>> create(Ref ref) {
return filteredPointsRecords(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AsyncValue<List<PointsRecord>> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AsyncValue<List<PointsRecord>>>(
value,
),
);
}
}
String _$filteredPointsRecordsHash() =>
r'afb0691b799f053b5c7fff2f8b64065917b5cd33';

View File

@@ -0,0 +1,654 @@
/// Model House Detail Page
///
/// Displays 360° view launcher, project information, and image gallery.
library;
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';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Model House Detail Page
class ModelHouseDetailPage extends ConsumerWidget {
final String modelId;
const ModelHouseDetailPage({
required this.modelId,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Mock data - in real app, fetch from provider
final modelData = _getMockData(modelId);
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
leading: IconButton(
icon: const FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
size: 20,
),
onPressed: () => context.pop(),
),
title: const Text(
'Chi tiết Nhà mẫu',
style: TextStyle(color: Colors.black),
),
actions: [
IconButton(
icon: const FaIcon(
FontAwesomeIcons.shareNodes,
color: Colors.black,
size: 20,
),
onPressed: () => _shareModel(context, modelData),
),
const SizedBox(width: AppSpacing.sm),
],
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
centerTitle: false,
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 360° View Launcher
_build360ViewLauncher(context, modelData),
const SizedBox(height: 16),
// Project Information
_buildProjectInfo(modelData),
const SizedBox(height: 16),
// Image Gallery
_buildImageGallery(context, modelData),
const SizedBox(height: 40),
],
),
),
);
}
Widget _build360ViewLauncher(
BuildContext context,
Map<String, dynamic> modelData,
) {
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _launch360View(context, modelData['url360'] as String),
borderRadius: BorderRadius.circular(12),
child: Container(
height: 400,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF667eea), Color(0xFF764ba2)],
),
borderRadius: BorderRadius.circular(12),
),
child: Stack(
children: [
// Background image with overlay
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Opacity(
opacity: 0.3,
child: CachedNetworkImage(
imageUrl: (modelData['images'] as List<Map<String, String>>)
.first['url']!,
fit: BoxFit.cover,
),
),
),
),
// Content
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 360° Icon
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.2),
border: Border.all(color: Colors.white, width: 3),
),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.arrowsRotate,
size: 40,
color: Colors.white,
),
SizedBox(height: 4),
Text(
'360°',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
const SizedBox(height: 20),
const Text(
'Xem nhà mẫu 360°',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black26,
offset: Offset(0, 2),
blurRadius: 4,
),
],
),
),
const SizedBox(height: 8),
const Text(
'Trải nghiệm không gian thực tế ảo',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
const SizedBox(height: 20),
// Launch Button
Container(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
FaIcon(
FontAwesomeIcons.play,
size: 14,
color: Color(0xFF667eea),
),
SizedBox(width: 8),
Text(
'Bắt đầu tham quan',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF667eea),
),
),
],
),
),
],
),
),
],
),
),
),
),
);
}
Widget _buildProjectInfo(Map<String, dynamic> modelData) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Text(
modelData['title'] as String,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
const SizedBox(height: 16),
// Specs Grid
Row(
children: [
Expanded(
child: _buildSpecItem(
'Diện tích',
modelData['area'] as String,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSpecItem(
'Địa điểm',
modelData['location'] as String,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSpecItem(
'Phong cách',
modelData['style'] as String,
),
),
],
),
const SizedBox(height: 20),
// Description
Text(
modelData['description'] as String,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF4b5563),
height: 1.6,
),
),
],
),
);
}
Widget _buildSpecItem(String label, String value) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(
label.toUpperCase(),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.grey500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildImageGallery(
BuildContext context,
Map<String, dynamic> modelData,
) {
final images = modelData['images'] as List<Map<String, String>>;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Gallery Title
const Row(
children: [
FaIcon(
FontAwesomeIcons.images,
size: 18,
color: AppColors.grey900,
),
SizedBox(width: 8),
Text(
'Thư viện Hình ảnh',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
],
),
const SizedBox(height: 16),
// Gallery Grid
SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: images.length,
itemBuilder: (context, index) {
final image = images[index];
return Padding(
padding: const EdgeInsets.only(right: 12),
child: GestureDetector(
onTap: () => _showImageViewer(context, images, index),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 120,
height: 120,
child: CachedNetworkImage(
imageUrl: image['url']!,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: AppColors.grey100,
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
),
errorWidget: (context, url, error) => Container(
color: AppColors.grey100,
child: const Icon(Icons.error),
),
),
),
),
),
);
},
),
),
],
),
);
}
Future<void> _launch360View(BuildContext context, String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Không thể mở link 360°')),
);
}
}
}
void _showImageViewer(
BuildContext context,
List<Map<String, String>> images,
int initialIndex,
) {
showDialog<void>(
context: context,
barrierColor: Colors.black87,
builder: (context) => _ImageViewerDialog(
images: images,
initialIndex: initialIndex,
),
);
}
void _shareModel(BuildContext context, Map<String, dynamic> modelData) {
Share.share(
'Xem mô hình 360° ${modelData['title']}\n${modelData['url360']}',
subject: modelData['title'] as String,
);
}
Map<String, dynamic> _getMockData(String modelId) {
// Mock data - in real app, fetch from repository
return {
'title': 'Căn hộ Studio',
'area': '35m²',
'location': 'Quận 7',
'style': 'Hiện đại',
'description':
'Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.',
'url360': 'https://vr.house3d.com/web/panorama-player/H00179549',
'images': [
{
'url':
'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=600&fit=crop',
'caption': 'Phối cảnh tổng thể căn hộ studio với thiết kế hiện đại',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/main_img.jpg',
'caption': 'Khu vực phòng khách với gạch granite cao cấp',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_1.jpg?v=1',
'caption': 'Phòng ngủ chính với gạch ceramic màu trung tính',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_0.jpg?v=1',
'caption': 'Khu vực bếp với gạch mosaic điểm nhấn',
},
{
'url':
'https://images.unsplash.com/photo-1620626011761-996317b8d101?w=800&h=600&fit=crop',
'caption': 'Phòng tắm hiện đại với gạch chống thấm cao cấp',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_3.jpg?v=1',
'caption': 'Khu vực bàn ăn ấm cúng',
},
],
};
}
}
/// Image Viewer Dialog with Swipe Navigation
class _ImageViewerDialog extends StatefulWidget {
final List<Map<String, String>> images;
final int initialIndex;
const _ImageViewerDialog({
required this.images,
required this.initialIndex,
});
@override
State<_ImageViewerDialog> createState() => _ImageViewerDialogState();
}
class _ImageViewerDialogState extends State<_ImageViewerDialog> {
late PageController _pageController;
late int _currentIndex;
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: Container(
color: Colors.black,
child: Stack(
children: [
// Main PageView
Center(
child: PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
itemCount: widget.images.length,
itemBuilder: (context, index) {
return Center(
child: CachedNetworkImage(
imageUrl: widget.images[index]['url']!,
fit: BoxFit.contain,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(color: Colors.white),
),
errorWidget: (context, url, error) => const Icon(
Icons.error,
color: Colors.white,
size: 48,
),
),
);
},
),
),
// Top bar with counter and close button
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.7),
Colors.transparent,
],
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_currentIndex + 1} / ${widget.images.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
],
),
),
),
),
// Caption at bottom
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.7),
],
),
),
child: Text(
widget.images[_currentIndex]['caption']!,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
),
],
),
),
);
}
}

View File

@@ -169,6 +169,7 @@ class _LibraryTab extends StatelessWidget {
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
children: const [ children: const [
_LibraryCard( _LibraryCard(
modelId: 'studio-01',
imageUrl: imageUrl:
'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=200&fit=crop', 'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=200&fit=crop',
title: 'Căn hộ Studio', title: 'Căn hộ Studio',
@@ -178,6 +179,7 @@ class _LibraryTab extends StatelessWidget {
has360View: true, has360View: true,
), ),
_LibraryCard( _LibraryCard(
modelId: 'villa-01',
imageUrl: imageUrl:
'https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=200&fit=crop', 'https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=200&fit=crop',
title: 'Biệt thự Hiện đại', title: 'Biệt thự Hiện đại',
@@ -187,6 +189,7 @@ class _LibraryTab extends StatelessWidget {
has360View: true, has360View: true,
), ),
_LibraryCard( _LibraryCard(
modelId: 'townhouse-01',
imageUrl: imageUrl:
'https://images.unsplash.com/photo-1562663474-6cbb3eaa4d14?w=800&h=200&fit=crop', 'https://images.unsplash.com/photo-1562663474-6cbb3eaa4d14?w=800&h=200&fit=crop',
title: 'Nhà phố Tối giản', title: 'Nhà phố Tối giản',
@@ -196,6 +199,7 @@ class _LibraryTab extends StatelessWidget {
has360View: true, has360View: true,
), ),
_LibraryCard( _LibraryCard(
modelId: 'apartment-01',
imageUrl: imageUrl:
'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&h=200&fit=crop', 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&h=200&fit=crop',
title: 'Chung cư Cao cấp', title: 'Chung cư Cao cấp',
@@ -212,6 +216,7 @@ class _LibraryTab extends StatelessWidget {
/// Library Card Widget /// Library Card Widget
class _LibraryCard extends StatelessWidget { class _LibraryCard extends StatelessWidget {
const _LibraryCard({ const _LibraryCard({
required this.modelId,
required this.imageUrl, required this.imageUrl,
required this.title, required this.title,
required this.date, required this.date,
@@ -219,6 +224,7 @@ class _LibraryCard extends StatelessWidget {
this.has360View = false, this.has360View = false,
}); });
final String modelId;
final String imageUrl; final String imageUrl;
final String title; final String title;
final String date; final String date;
@@ -233,13 +239,7 @@ class _LibraryCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 20), margin: const EdgeInsets.only(bottom: 20),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
ScaffoldMessenger.of(context).showSnackBar( context.push('/model-houses/$modelId');
const SnackBar(
content: Text(
'Chức năng xem chi tiết sẽ được triển khai trong phiên bản tiếp theo',
),
),
);
}, },
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Column( child: Column(