final final part 1
This commit is contained in:
parent
6646955726
commit
8cc8d14ad6
@ -2,5 +2,6 @@ class AppConfig {
|
|||||||
const AppConfig._();
|
const AppConfig._();
|
||||||
|
|
||||||
static const String apiBaseUrl = 'https://api-tms-uat.kbzbank.com:8443';
|
static const String apiBaseUrl = 'https://api-tms-uat.kbzbank.com:8443';
|
||||||
|
// static const String apiBaseUrl = 'https://receipt-nest.utsmyanmar.com';
|
||||||
static const String apiSecret = 'y812J21lhha11OS';
|
static const String apiSecret = 'y812J21lhha11OS';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,10 +19,14 @@ class ApiTransactionRepository implements TransactionRepository {
|
|||||||
Future<List<TransactionRecord>> getTransactions({
|
Future<List<TransactionRecord>> getTransactions({
|
||||||
required String token,
|
required String token,
|
||||||
required String serial,
|
required String serial,
|
||||||
|
required String range,
|
||||||
}) async {
|
}) async {
|
||||||
final uri = Uri.parse(
|
final uri = Uri.parse('$baseUrl/receipt/transaction').replace(
|
||||||
'$baseUrl/receipt/transaction',
|
queryParameters: {
|
||||||
).replace(queryParameters: {'serial': serial});
|
'serial': serial,
|
||||||
|
'range': range,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final response = await _client.get(
|
final response = await _client.get(
|
||||||
uri,
|
uri,
|
||||||
@ -88,11 +92,11 @@ class ApiTransactionRepository implements TransactionRepository {
|
|||||||
|
|
||||||
return TransactionRecord(
|
return TransactionRecord(
|
||||||
id: _pickString(item, const ['id', 'transactionId', 'transaction_id']),
|
id: _pickString(item, const ['id', 'transactionId', 'transaction_id']),
|
||||||
rrn: _pickString(item, const ['rrn', 'referenceNo', 'reference_no']),
|
rrn: _pickString(item, const ['rrn', 'referenceNo', 'reference_no', 'DE37']),
|
||||||
amount: _pickNum(item, const ['amount', 'totalAmount', 'total_amount']),
|
amount: _pickNum(item, const ['amount', 'totalAmount', 'total_amount', 'DE4']),
|
||||||
status: _pickString(item, const ['status']),
|
status: _pickString(item, const ['status', 'description', 'DE39']),
|
||||||
type: _pickString(item, const ['type', 'transactionType']),
|
type: _pickString(item, const ['type', 'transactionType', 'DE3']),
|
||||||
createdAt: _pickString(item, const ['createdAt', 'created_at']),
|
createdAt: _pickString(item, const ['createdAt', 'created_at', 'CREATED_AT']),
|
||||||
raw: item,
|
raw: item,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,5 +4,6 @@ abstract class TransactionRepository {
|
|||||||
Future<List<TransactionRecord>> getTransactions({
|
Future<List<TransactionRecord>> getTransactions({
|
||||||
required String token,
|
required String token,
|
||||||
required String serial,
|
required String serial,
|
||||||
|
required String range,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -184,9 +184,18 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () => ref.refresh(merchantListProvider.future),
|
||||||
child: merchants.isEmpty
|
child: merchants.isEmpty
|
||||||
? const Center(child: Text('No merchants found'))
|
? ListView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
children: const [
|
||||||
|
SizedBox(height: 120),
|
||||||
|
Center(child: Text('No merchants found')),
|
||||||
|
],
|
||||||
|
)
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
itemCount: merchants.length,
|
itemCount: merchants.length,
|
||||||
separatorBuilder: (_, __) =>
|
separatorBuilder: (_, __) =>
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@ -212,6 +221,7 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||||
|
import 'package:e_receipt_mobile/presentation/terminal/transaction_receipt_screen.dart';
|
||||||
import 'package:e_receipt_mobile/presentation/terminal/transaction_view_model.dart';
|
import 'package:e_receipt_mobile/presentation/terminal/transaction_view_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class TerminalNextScreen extends ConsumerWidget {
|
class TerminalNextScreen extends ConsumerStatefulWidget {
|
||||||
const TerminalNextScreen({
|
const TerminalNextScreen({
|
||||||
required this.merchantName,
|
required this.merchantName,
|
||||||
required this.terminal,
|
required this.terminal,
|
||||||
@ -14,11 +15,23 @@ class TerminalNextScreen extends ConsumerWidget {
|
|||||||
final Terminal terminal;
|
final Terminal terminal;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<TerminalNextScreen> createState() => _TerminalNextScreenState();
|
||||||
final serial = terminal.serial?.trim() ?? '';
|
}
|
||||||
|
|
||||||
|
class _TerminalNextScreenState extends ConsumerState<TerminalNextScreen> {
|
||||||
|
static const List<String> _ranges = <String>['1d', '1w', '1m'];
|
||||||
|
String _selectedRange = '1d';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final serial = widget.terminal.serial?.trim() ?? '';
|
||||||
final hasSerial = serial.isNotEmpty;
|
final hasSerial = serial.isNotEmpty;
|
||||||
final transactionsAsync = hasSerial
|
final transactionsAsync = hasSerial
|
||||||
? ref.watch(transactionListProvider(serial))
|
? ref.watch(
|
||||||
|
transactionListProvider(
|
||||||
|
TransactionQuery(serial: serial, range: _selectedRange),
|
||||||
|
),
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -29,26 +42,54 @@ class TerminalNextScreen extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Merchant: $merchantName',
|
'Merchant: ${widget.merchantName}',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text('Terminal: ${terminal.name ?? terminal.tid ?? '-'}'),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text('TID: ${terminal.tid ?? '-'}'),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
Text(
|
||||||
'MIDs: ${(terminal.mids == null || terminal.mids!.isEmpty) ? '-' : terminal.mids!.join(', ')}',
|
'Terminal: ${widget.terminal.name ?? widget.terminal.tid ?? '-'}',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text('Serial: ${terminal.serial ?? '-'}'),
|
Text('TID: ${widget.terminal.tid ?? '-'}'),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text('Address: ${terminal.address ?? '-'}'),
|
Text(
|
||||||
|
'MIDs: ${(widget.terminal.mids == null || widget.terminal.mids!.isEmpty) ? '-' : widget.terminal.mids!.join(', ')}',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('Serial: ${widget.terminal.serial ?? '-'}'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('Address: ${widget.terminal.address ?? '-'}'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Transactions',
|
'Transactions',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
|
const Spacer(),
|
||||||
|
const Text('Range: '),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
DropdownButton<String>(
|
||||||
|
value: _selectedRange,
|
||||||
|
items: _ranges
|
||||||
|
.map(
|
||||||
|
(range) => DropdownMenuItem<String>(
|
||||||
|
value: range,
|
||||||
|
child: Text(range),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null || value == _selectedRange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_selectedRange = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: !hasSerial
|
child: !hasSerial
|
||||||
@ -62,13 +103,27 @@ class TerminalNextScreen extends ConsumerWidget {
|
|||||||
child: Text('Failed to load transactions: $error'),
|
child: Text('Failed to load transactions: $error'),
|
||||||
),
|
),
|
||||||
data: (transactions) {
|
data: (transactions) {
|
||||||
if (transactions.isEmpty) {
|
return RefreshIndicator(
|
||||||
return const Center(
|
onRefresh: () => ref.refresh(
|
||||||
child: Text('No transactions found'),
|
transactionListProvider(
|
||||||
);
|
TransactionQuery(
|
||||||
}
|
serial: serial,
|
||||||
|
range: _selectedRange,
|
||||||
return ListView.separated(
|
),
|
||||||
|
).future,
|
||||||
|
),
|
||||||
|
child: transactions.isEmpty
|
||||||
|
? ListView(
|
||||||
|
physics:
|
||||||
|
const AlwaysScrollableScrollPhysics(),
|
||||||
|
children: const [
|
||||||
|
SizedBox(height: 120),
|
||||||
|
Center(child: Text('No transactions found')),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: ListView.separated(
|
||||||
|
physics:
|
||||||
|
const AlwaysScrollableScrollPhysics(),
|
||||||
itemCount: transactions.length,
|
itemCount: transactions.length,
|
||||||
separatorBuilder: (_, __) =>
|
separatorBuilder: (_, __) =>
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@ -76,15 +131,31 @@ class TerminalNextScreen extends ConsumerWidget {
|
|||||||
final item = transactions[index];
|
final item = transactions[index];
|
||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(
|
onTap: () {
|
||||||
'Amount: ${item.amount?.toString() ?? '-'}',
|
Navigator.of(context).push(
|
||||||
),
|
MaterialPageRoute<void>(
|
||||||
subtitle: Text(
|
builder: (_) =>
|
||||||
'Status: ${item.status ?? '-'} | RRN: ${item.rrn ?? '-'}',
|
TransactionReceiptScreen(
|
||||||
|
merchantName:
|
||||||
|
widget.merchantName,
|
||||||
|
terminal: widget.terminal,
|
||||||
|
transaction: item,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
title: Text(
|
||||||
|
'Amount: ${item.amount?.toString() ?? '-'}',
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Status: ${item.status ?? '-'} | Type: ${item.type ?? '-'} | RRN: ${item.rrn ?? '-'}\nTap to view receipt detail',
|
||||||
|
),
|
||||||
|
trailing:
|
||||||
|
const Icon(Icons.receipt_long_outlined),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@ -33,11 +33,20 @@ class _TerminalSelectionScreenState
|
|||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (error, _) => Center(child: Text('Failed to load terminals: $error')),
|
error: (error, _) => Center(child: Text('Failed to load terminals: $error')),
|
||||||
data: (terminals) {
|
data: (terminals) {
|
||||||
if (terminals.isEmpty) {
|
return RefreshIndicator(
|
||||||
return const Center(child: Text('No terminals found'));
|
onRefresh: () =>
|
||||||
}
|
ref.refresh(terminalListProvider(widget.merchantId).future),
|
||||||
|
child: terminals.isEmpty
|
||||||
return ListView.separated(
|
? ListView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: const [
|
||||||
|
SizedBox(height: 120),
|
||||||
|
Center(child: Text('No terminals found')),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: ListView.separated(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: terminals.length,
|
itemCount: terminals.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
@ -55,6 +64,7 @@ class _TerminalSelectionScreenState
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
144
lib/presentation/terminal/transaction_receipt_screen.dart
Normal file
144
lib/presentation/terminal/transaction_receipt_screen.dart
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||||
|
import 'package:e_receipt_mobile/domain/entities/transaction_record.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TransactionReceiptScreen extends StatelessWidget {
|
||||||
|
const TransactionReceiptScreen({
|
||||||
|
required this.merchantName,
|
||||||
|
required this.terminal,
|
||||||
|
required this.transaction,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String merchantName;
|
||||||
|
final Terminal terminal;
|
||||||
|
final TransactionRecord transaction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final raw = transaction.raw ?? const <String, dynamic>{};
|
||||||
|
final amount = _first(raw, const ['DE4']) ?? transaction.amount?.toString();
|
||||||
|
final currency = _first(raw, const ['DE49']) ?? 'MMK';
|
||||||
|
final status = _statusLabel(
|
||||||
|
_first(raw, const ['description', 'DE39', 'status']) ?? transaction.status,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Receipt Detail')),
|
||||||
|
body: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: DefaultTextStyle(
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium!,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'E-Receipt',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Center(child: Text(merchantName)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text('--------------------------------'),
|
||||||
|
_line('Terminal', _first(raw, const ['DE41']) ?? terminal.tid),
|
||||||
|
_line('Merchant ID', _first(raw, const ['DE42'])),
|
||||||
|
_line('Serial', terminal.serial),
|
||||||
|
_line(
|
||||||
|
'Invoice',
|
||||||
|
_first(raw, const ['invoice_number', 'invoiceNo']),
|
||||||
|
),
|
||||||
|
_line('Date/Time', _first(raw, const ['CREATED_AT', 'de7_date'])),
|
||||||
|
_line('Trace No', _first(raw, const ['DE11'])),
|
||||||
|
_line('RRN', _first(raw, const ['DE37']) ?? transaction.rrn),
|
||||||
|
_line('Auth Code', _first(raw, const ['DE38'])),
|
||||||
|
_line('Type', _first(raw, const ['DE3']) ?? transaction.type),
|
||||||
|
const Text('--------------------------------'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'AMOUNT',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${amount ?? '-'} $currency',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
status,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: status == 'SUCCESS'
|
||||||
|
? Colors.green.shade700
|
||||||
|
: Colors.red.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text('--------------------------------'),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'Thank you',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _line(String label, String? value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(label)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value == null || value.trim().isEmpty ? '-' : value,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _first(Map<String, dynamic> raw, List<String> keys) {
|
||||||
|
for (final key in keys) {
|
||||||
|
final value = raw[key];
|
||||||
|
if (value != null) {
|
||||||
|
final text = value.toString().trim();
|
||||||
|
if (text.isNotEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _statusLabel(String? status) {
|
||||||
|
final normalized = (status ?? '').trim().toUpperCase();
|
||||||
|
if (normalized == 'A' || normalized == 'SUCCESS' || normalized == 'PAY_SUCCESS') {
|
||||||
|
return 'SUCCESS';
|
||||||
|
}
|
||||||
|
if (normalized == 'E' || normalized == 'FAILED') {
|
||||||
|
return 'FAILED';
|
||||||
|
}
|
||||||
|
return normalized.isEmpty ? '-' : normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,14 +14,38 @@ final transactionRepositoryProvider = Provider<TransactionRepository>((ref) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class TransactionQuery {
|
||||||
|
const TransactionQuery({required this.serial, required this.range});
|
||||||
|
|
||||||
|
final String serial;
|
||||||
|
final String range;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return other is TransactionQuery &&
|
||||||
|
other.serial == serial &&
|
||||||
|
other.range == range;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(serial, range);
|
||||||
|
}
|
||||||
|
|
||||||
final transactionListProvider =
|
final transactionListProvider =
|
||||||
FutureProvider.family<List<TransactionRecord>, String>((ref, serial) async {
|
FutureProvider.family<List<TransactionRecord>, TransactionQuery>(
|
||||||
|
(ref, query) async {
|
||||||
final sessionUser = ref.watch(sessionControllerProvider);
|
final sessionUser = ref.watch(sessionControllerProvider);
|
||||||
if (sessionUser == null) {
|
if (sessionUser == null) {
|
||||||
throw Exception('No active session');
|
throw Exception('No active session');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ref
|
return ref.watch(transactionRepositoryProvider).getTransactions(
|
||||||
.watch(transactionRepositoryProvider)
|
token: sessionUser.token,
|
||||||
.getTransactions(token: sessionUser.token, serial: serial);
|
serial: query.serial,
|
||||||
});
|
range: query.range,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user