list with cards

This commit is contained in:
MooN 2026-02-14 17:16:58 +06:30
parent b62d4c56ea
commit c264c546f1
5 changed files with 248 additions and 11 deletions

View File

@ -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<List<Merchant>> 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<Merchant> _parseMerchants(String body) {
final decoded = _tryDecode(body);
final list = _extractList(decoded);
return list.map(_merchantFromItem).whereType<Merchant>().toList();
}
dynamic _tryDecode(String body) {
try {
return jsonDecode(body);
} catch (_) {
return null;
}
}
List<dynamic> _extractList(dynamic decoded) {
if (decoded is List<dynamic>) {
return decoded;
}
if (decoded is Map<String, dynamic>) {
final directMerchants = decoded['merchants'];
if (directMerchants is List<dynamic>) {
return directMerchants;
}
final data =
decoded['data'] ??
decoded['result'] ??
decoded['payload'] ??
decoded['merchantData'];
if (data is List<dynamic>) {
return data;
}
if (data is Map<String, dynamic>) {
final nestedMerchants = data['merchants'];
if (nestedMerchants is List<dynamic>) {
return nestedMerchants;
}
}
}
return const <dynamic>[];
}
Merchant? _merchantFromItem(dynamic item) {
if (item is! Map<String, dynamic>) {
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<String, dynamic> data, List<String> 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<String, dynamic>) {
final message = decoded['message'] ?? decoded['error'] ?? decoded['detail'];
if (message != null) {
return message.toString();
}
}
return 'Failed to load merchants';
}
}

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
import 'package:e_receipt_mobile/domain/entities/merchant.dart';
abstract class MerchantRepository {
Future<List<Merchant>> getMerchants({required String token});
}

View File

@ -1,27 +1,81 @@
import 'package:e_receipt_mobile/domain/entities/login_user.dart'; 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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class HomeScreen extends StatelessWidget { class HomeScreen extends ConsumerWidget {
const HomeScreen({required this.user, super.key}); const HomeScreen({required this.user, super.key});
final LoginUser user; final LoginUser user;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final role = user.role.toLowerCase(); final role = user.role.toLowerCase();
final merchantsAsync = ref.watch(merchantListProvider);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(role == 'admin' ? 'Admin Home' : 'User Home'), title: Text("Merchants"),
centerTitle: true, centerTitle: true,
), ),
body: Center( body: Padding(
child: Column( padding: const EdgeInsets.all(16),
mainAxisSize: MainAxisSize.min, child: merchantsAsync.when(
children: [ loading: () => const Center(child: CircularProgressIndicator()),
Text('Welcome ${user.username}'), error: (error, _) => Center(
const SizedBox(height: 8), child: Text('Failed to load merchants: $error'),
Text('Role: ${user.role}'), ),
], 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,
),
],
),
),
);
},
),
),
],
);
},
), ),
), ),
); );

View File

@ -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<MerchantRepository>((ref) {
return ApiMerchantRepository(
baseUrl: AppConfig.apiBaseUrl,
apiSecret: AppConfig.apiSecret,
client: ref.watch(authenticatedHttpClientProvider),
);
});
final merchantListProvider = FutureProvider<List<Merchant>>((ref) async {
final sessionUser = ref.watch(sessionControllerProvider);
if (sessionUser == null) {
throw Exception('No active session');
}
return ref
.watch(merchantRepositoryProvider)
.getMerchants(token: sessionUser.token);
});