From c264c546f1331aa037ac55443319fae8c12c1754 Mon Sep 17 00:00:00 2001 From: MooN <56061215+MgKyawLay@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:16:58 +0630 Subject: [PATCH] list with cards --- .../repositories/api_merchant_repository.dart | 121 ++++++++++++++++++ lib/domain/entities/merchant.dart | 31 +++++ .../repositories/merchant_repository.dart | 5 + lib/presentation/home/home_screen.dart | 76 +++++++++-- lib/presentation/home/home_view_model.dart | 26 ++++ 5 files changed, 248 insertions(+), 11 deletions(-) create mode 100644 lib/data/repositories/api_merchant_repository.dart create mode 100644 lib/domain/entities/merchant.dart create mode 100644 lib/domain/repositories/merchant_repository.dart create mode 100644 lib/presentation/home/home_view_model.dart diff --git a/lib/data/repositories/api_merchant_repository.dart b/lib/data/repositories/api_merchant_repository.dart new file mode 100644 index 0000000..843609e --- /dev/null +++ b/lib/data/repositories/api_merchant_repository.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; + +import 'package:e_receipt_mobile/domain/entities/merchant.dart'; +import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart'; +import 'package:http/http.dart' as http; + +class ApiMerchantRepository implements MerchantRepository { + ApiMerchantRepository({ + required this.baseUrl, + required this.apiSecret, + required http.Client client, + }) : _client = client; + + final String baseUrl; + final String apiSecret; + final http.Client _client; + + @override + Future> getMerchants({required String token}) async { + final uri = Uri.parse('$baseUrl/receipt/merchant'); + final response = await _client.get( + uri, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiSecret, + 'Authorization': 'Bearer ${token.trim()}', + }, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return _parseMerchants(response.body); + } + + throw Exception(_extractErrorMessage(response.body)); + } + + List _parseMerchants(String body) { + final decoded = _tryDecode(body); + final list = _extractList(decoded); + return list.map(_merchantFromItem).whereType().toList(); + } + + dynamic _tryDecode(String body) { + try { + return jsonDecode(body); + } catch (_) { + return null; + } + } + + List _extractList(dynamic decoded) { + if (decoded is List) { + return decoded; + } + if (decoded is Map) { + final directMerchants = decoded['merchants']; + if (directMerchants is List) { + return directMerchants; + } + + final data = + decoded['data'] ?? + decoded['result'] ?? + decoded['payload'] ?? + decoded['merchantData']; + if (data is List) { + return data; + } + if (data is Map) { + final nestedMerchants = data['merchants']; + if (nestedMerchants is List) { + return nestedMerchants; + } + } + } + return const []; + } + + Merchant? _merchantFromItem(dynamic item) { + if (item is! Map) { + return null; + } + + return Merchant( + id: _pickString(item, const ['id', 'merchantId', 'merchant_id']), + name: _pickString(item, const ['name', 'merchantName', 'merchant_name']), + mobile: _pickString(item, const ['mobile']), + description: _pickString(item, const ['description']), + address: _pickString(item, const ['address']), + address2: _pickString(item, const ['address2']), + address3: _pickString(item, const ['address3']), + phone: _pickString(item, const ['phone']), + mids: _pickString(item, const ['mids']), + createdBy: _pickString(item, const ['createdBy', 'created_by']), + updatedBy: _pickString(item, const ['updatedBy', 'updated_by']), + createdAt: _pickString(item, const ['createdAt', 'created_at']), + updatedAt: _pickString(item, const ['updatedAt', 'updated_at']), + ); + } + + String? _pickString(Map data, List keys) { + for (final key in keys) { + final value = data[key]; + if (value != null && value.toString().trim().isNotEmpty) { + return value.toString().trim(); + } + } + return null; + } + + String _extractErrorMessage(String body) { + final decoded = _tryDecode(body); + if (decoded is Map) { + final message = decoded['message'] ?? decoded['error'] ?? decoded['detail']; + if (message != null) { + return message.toString(); + } + } + return 'Failed to load merchants'; + } +} diff --git a/lib/domain/entities/merchant.dart b/lib/domain/entities/merchant.dart new file mode 100644 index 0000000..6482bc0 --- /dev/null +++ b/lib/domain/entities/merchant.dart @@ -0,0 +1,31 @@ +class Merchant { + const Merchant({ + this.id, + this.name, + this.mobile, + this.description, + this.address, + this.address2, + this.address3, + this.phone, + this.mids, + this.createdBy, + this.updatedBy, + this.createdAt, + this.updatedAt, + }); + + final String? id; + final String? name; + final String? mobile; + final String? description; + final String? address; + final String? address2; + final String? address3; + final String? phone; + final String? mids; + final String? createdBy; + final String? updatedBy; + final String? createdAt; + final String? updatedAt; +} diff --git a/lib/domain/repositories/merchant_repository.dart b/lib/domain/repositories/merchant_repository.dart new file mode 100644 index 0000000..ef08496 --- /dev/null +++ b/lib/domain/repositories/merchant_repository.dart @@ -0,0 +1,5 @@ +import 'package:e_receipt_mobile/domain/entities/merchant.dart'; + +abstract class MerchantRepository { + Future> getMerchants({required String token}); +} diff --git a/lib/presentation/home/home_screen.dart b/lib/presentation/home/home_screen.dart index bc60389..3527918 100644 --- a/lib/presentation/home/home_screen.dart +++ b/lib/presentation/home/home_screen.dart @@ -1,27 +1,81 @@ import 'package:e_receipt_mobile/domain/entities/login_user.dart'; +import 'package:e_receipt_mobile/presentation/home/home_view_model.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class HomeScreen extends StatelessWidget { +class HomeScreen extends ConsumerWidget { const HomeScreen({required this.user, super.key}); final LoginUser user; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final role = user.role.toLowerCase(); + final merchantsAsync = ref.watch(merchantListProvider); + return Scaffold( appBar: AppBar( - title: Text(role == 'admin' ? 'Admin Home' : 'User Home'), + title: Text("Merchants"), centerTitle: true, ), - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Welcome ${user.username}'), - const SizedBox(height: 8), - Text('Role: ${user.role}'), - ], + body: Padding( + padding: const EdgeInsets.all(16), + child: merchantsAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center( + child: Text('Failed to load merchants: $error'), + ), + data: (merchants) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Total (${merchants.length})', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Expanded( + child: merchants.isEmpty + ? const Center(child: Text('No merchants found')) + : ListView.separated( + itemCount: merchants.length, + separatorBuilder: (_, __) => + const SizedBox(height: 10), + itemBuilder: (context, index) { + final merchant = merchants[index]; + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${index + 1}. ${merchant.name ?? '-'}', + style: Theme.of(context) + .textTheme + .titleMedium, + ), + const SizedBox(height: 6), + Text( + merchant.address ?? '-', + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ], + ), + ), + ); + }, + ), + ), + ], + ); + }, ), ), ); diff --git a/lib/presentation/home/home_view_model.dart b/lib/presentation/home/home_view_model.dart new file mode 100644 index 0000000..2cddc1a --- /dev/null +++ b/lib/presentation/home/home_view_model.dart @@ -0,0 +1,26 @@ +import 'package:e_receipt_mobile/core/config/app_config.dart'; +import 'package:e_receipt_mobile/data/repositories/api_merchant_repository.dart'; +import 'package:e_receipt_mobile/domain/entities/merchant.dart'; +import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final merchantRepositoryProvider = Provider((ref) { + return ApiMerchantRepository( + baseUrl: AppConfig.apiBaseUrl, + apiSecret: AppConfig.apiSecret, + client: ref.watch(authenticatedHttpClientProvider), + ); +}); + +final merchantListProvider = FutureProvider>((ref) async { + final sessionUser = ref.watch(sessionControllerProvider); + if (sessionUser == null) { + throw Exception('No active session'); + } + + return ref + .watch(merchantRepositoryProvider) + .getMerchants(token: sessionUser.token); +});