list with cards
This commit is contained in:
parent
b62d4c56ea
commit
c264c546f1
121
lib/data/repositories/api_merchant_repository.dart
Normal file
121
lib/data/repositories/api_merchant_repository.dart
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/domain/entities/merchant.dart
Normal file
31
lib/domain/entities/merchant.dart
Normal 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;
|
||||||
|
}
|
||||||
5
lib/domain/repositories/merchant_repository.dart
Normal file
5
lib/domain/repositories/merchant_repository.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import 'package:e_receipt_mobile/domain/entities/merchant.dart';
|
||||||
|
|
||||||
|
abstract class MerchantRepository {
|
||||||
|
Future<List<Merchant>> getMerchants({required String token});
|
||||||
|
}
|
||||||
@ -1,29 +1,83 @@
|
|||||||
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(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, _) => Center(
|
||||||
|
child: Text('Failed to load merchants: $error'),
|
||||||
|
),
|
||||||
|
data: (merchants) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('Welcome ${user.username}'),
|
Text(
|
||||||
|
'Total (${merchants.length})',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text('Role: ${user.role}'),
|
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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
lib/presentation/home/home_view_model.dart
Normal file
26
lib/presentation/home/home_view_model.dart
Normal 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);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user