Can update scanned expenses entries
This commit is contained in:
		
							
								
								
									
										69
									
								
								moneymgr_mobile/lib/routes/scan_details/scan_details.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								moneymgr_mobile/lib/routes/scan_details/scan_details.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:logging/logging.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/services/storage/expenses.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/utils/extensions.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/widgets/expense_editor.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/widgets/full_screen_error.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/widgets/loading_scaffold.dart';
 | 
				
			||||||
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part 'scan_details.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@riverpod
 | 
				
			||||||
 | 
					Future<(Expense, Uint8List)?> _getExpense(Ref ref, {required int id}) async {
 | 
				
			||||||
 | 
					  final expProvider = ref.watch(expensesProvider).requireValue;
 | 
				
			||||||
 | 
					  final expense = await expProvider.getById(id);
 | 
				
			||||||
 | 
					  if (expense == null) return null;
 | 
				
			||||||
 | 
					  final file = await expProvider.loadFile(expense);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (expense, file);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ScanDetailScreen extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  final int id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const ScanDetailScreen({super.key, required this.id});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final expenses = ref.watch(expensesProvider).requireValue;
 | 
				
			||||||
 | 
					    final expense = ref.watch(_getExpenseProvider(id: id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleUpdate(BaseExpenseInfo newInfo) async {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        await expenses.updateExpense(expense.requireValue!.$1, newInfo);
 | 
				
			||||||
 | 
					        if (context.mounted) {
 | 
				
			||||||
 | 
					          context.pop();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ref.invalidate(expensesProvider);
 | 
				
			||||||
 | 
					      } catch (e, s) {
 | 
				
			||||||
 | 
					        Logger.root.warning("Failed to update expense! $e$s");
 | 
				
			||||||
 | 
					        if (context.mounted) {
 | 
				
			||||||
 | 
					          context.showTextSnackBar("Failed to update expense! $e");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return switch (expense) {
 | 
				
			||||||
 | 
					      AsyncData(:final value) when value == null => FullScreenError(
 | 
				
			||||||
 | 
					        message: "Expense does not exists!",
 | 
				
			||||||
 | 
					        error: 'NONE',
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      AsyncData(:final value) => ExpenseEditor(
 | 
				
			||||||
 | 
					        file: value!.$2,
 | 
				
			||||||
 | 
					        onFinished: handleUpdate,
 | 
				
			||||||
 | 
					        initialData: value.$1.baseExpense,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      AsyncError(:final error) => FullScreenError(
 | 
				
			||||||
 | 
					        message: "Failed to load expense information!",
 | 
				
			||||||
 | 
					        error: error.toString(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      _ => LoadingScaffold(title: "Expense $id"),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,7 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/services/router/routes_list.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/services/storage/expenses.dart';
 | 
					import 'package:moneymgr_mobile/services/storage/expenses.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/utils/time_utils.dart';
 | 
					import 'package:moneymgr_mobile/utils/time_utils.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
@@ -37,9 +39,10 @@ class _ExpensesList extends StatelessWidget {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return ListView.builder(
 | 
					    return ListView.builder(
 | 
				
			||||||
      itemBuilder: (context, id) {
 | 
					      itemBuilder: (context, entryNum) {
 | 
				
			||||||
        final expense = list[id];
 | 
					        final expense = list[entryNum];
 | 
				
			||||||
        return ListTile(
 | 
					        return ListTile(
 | 
				
			||||||
 | 
					          onTap: () => context.push("$scansPage/${expense.id}"),
 | 
				
			||||||
          leading: Icon(Icons.receipt_long),
 | 
					          leading: Icon(Icons.receipt_long),
 | 
				
			||||||
          title: Text(
 | 
					          title: Text(
 | 
				
			||||||
            expense.label ?? "No label",
 | 
					            expense.label ?? "No label",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ import 'package:moneymgr_mobile/routes/login/manual_auth_screen.dart';
 | 
				
			|||||||
import 'package:moneymgr_mobile/routes/login/qr_auth_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/login/qr_auth_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/profile/profile_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/profile/profile_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/scan/scan_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/scan/scan_screen.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/routes/scan_details/scan_details.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/scans_list/scans_list_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/scans_list/scans_list_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/settings/settings_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/settings/settings_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/services/router/routes_list.dart';
 | 
					import 'package:moneymgr_mobile/services/router/routes_list.dart';
 | 
				
			||||||
@@ -36,7 +37,7 @@ GoRouter router(Ref ref) {
 | 
				
			|||||||
  // see [AuthState] enum.
 | 
					  // see [AuthState] enum.
 | 
				
			||||||
  final navigationItems = [
 | 
					  final navigationItems = [
 | 
				
			||||||
    NavigationItem(
 | 
					    NavigationItem(
 | 
				
			||||||
      path: scanPage,
 | 
					      path: capturePage,
 | 
				
			||||||
      body: (_) => ScanScreen(),
 | 
					      body: (_) => ScanScreen(),
 | 
				
			||||||
      icon: Icons.camera_alt_outlined,
 | 
					      icon: Icons.camera_alt_outlined,
 | 
				
			||||||
      selectedIcon: Icons.camera_alt,
 | 
					      selectedIcon: Icons.camera_alt,
 | 
				
			||||||
@@ -48,6 +49,15 @@ GoRouter router(Ref ref) {
 | 
				
			|||||||
      icon: Icons.list,
 | 
					      icon: Icons.list,
 | 
				
			||||||
      selectedIcon: Icons.list_alt,
 | 
					      selectedIcon: Icons.list_alt,
 | 
				
			||||||
      label: "List",
 | 
					      label: "List",
 | 
				
			||||||
 | 
					      routes: [
 | 
				
			||||||
 | 
					        GoRoute(
 | 
				
			||||||
 | 
					          path: ":id",
 | 
				
			||||||
 | 
					          builder: (_, state) {
 | 
				
			||||||
 | 
					            final id = int.parse(state.pathParameters["id"]!);
 | 
				
			||||||
 | 
					            return ScanDetailScreen(id: id);
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    NavigationItem(
 | 
					    NavigationItem(
 | 
				
			||||||
      path: profilePage,
 | 
					      path: profilePage,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@ const manualAuthPage = "/login/manual";
 | 
				
			|||||||
const settingsPage = "/settings";
 | 
					const settingsPage = "/settings";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Scan path
 | 
					/// Scan path
 | 
				
			||||||
const scanPage = "/scan";
 | 
					const capturePage = "/scan";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Scans page
 | 
					/// Scans page
 | 
				
			||||||
const scansPage = "/scans";
 | 
					const scansPage = "/scans";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import 'dart:convert';
 | 
					import 'dart:convert';
 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
import 'dart:math';
 | 
					import 'dart:math';
 | 
				
			||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
					import 'package:freezed_annotation/freezed_annotation.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
@@ -8,17 +9,24 @@ import 'package:path/path.dart' as p;
 | 
				
			|||||||
import 'package:path_provider/path_provider.dart';
 | 
					import 'package:path_provider/path_provider.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'expenses.freezed.dart';part 'expenses.g.dart';
 | 
					part 'expenses.freezed.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part 'expenses.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef ExpensesList = List<Expense>;
 | 
					typedef ExpensesList = List<Expense>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@freezed
 | 
					@freezed
 | 
				
			||||||
abstract class BaseExpenseInfo with _$BaseExpenseInfo {
 | 
					abstract class BaseExpenseInfo with _$BaseExpenseInfo {
 | 
				
			||||||
 | 
					  const BaseExpenseInfo._();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const factory BaseExpenseInfo({
 | 
					  const factory BaseExpenseInfo({
 | 
				
			||||||
    required String? label,
 | 
					    required String? label,
 | 
				
			||||||
    required double cost,
 | 
					    required double cost,
 | 
				
			||||||
    required DateTime time,
 | 
					    required DateTime time,
 | 
				
			||||||
  }) = _BaseExpenseInfo;
 | 
					  }) = _BaseExpenseInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Get expense time as second
 | 
				
			||||||
 | 
					  int get timeAsSeconds => (time.millisecondsSinceEpoch / 1000).floor();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@freezed
 | 
					@freezed
 | 
				
			||||||
@@ -45,6 +53,10 @@ abstract class Expense with _$Expense {
 | 
				
			|||||||
  factory Expense.fromJson(Map<String, dynamic> json) =>
 | 
					  factory Expense.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
      _$ExpenseFromJson(json);
 | 
					      _$ExpenseFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Get base expense information
 | 
				
			||||||
 | 
					  BaseExpenseInfo get baseExpense =>
 | 
				
			||||||
 | 
					      BaseExpenseInfo(label: label, cost: cost, time: dateTime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Get associated expense file name
 | 
					  /// Get associated expense file name
 | 
				
			||||||
  String get localFileName {
 | 
					  String get localFileName {
 | 
				
			||||||
    if (mimeType == "application/pdf") return "$id.pdf";
 | 
					    if (mimeType == "application/pdf") return "$id.pdf";
 | 
				
			||||||
@@ -109,7 +121,7 @@ class ExpensesManager {
 | 
				
			|||||||
      id: (list.lastOrNull?.id ?? 0) + Random().nextInt(1000),
 | 
					      id: (list.lastOrNull?.id ?? 0) + Random().nextInt(1000),
 | 
				
			||||||
      label: info.label,
 | 
					      label: info.label,
 | 
				
			||||||
      cost: info.cost,
 | 
					      cost: info.cost,
 | 
				
			||||||
      time: (info.time.millisecondsSinceEpoch / 1000).floor(),
 | 
					      time: info.timeAsSeconds,
 | 
				
			||||||
      mimeType: fileMimeType,
 | 
					      mimeType: fileMimeType,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -128,4 +140,28 @@ class ExpensesManager {
 | 
				
			|||||||
    list.add(exp);
 | 
					    list.add(exp);
 | 
				
			||||||
    await saveList(list);
 | 
					    await saveList(list);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Get a single expense information by its ID
 | 
				
			||||||
 | 
					  Future<Expense?> getById(int id) async {
 | 
				
			||||||
 | 
					    final list = await getList();
 | 
				
			||||||
 | 
					    return list.firstWhere((e) => e.id == id);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Load the file associated with an expense
 | 
				
			||||||
 | 
					  Future<Uint8List> loadFile(Expense expense) async {
 | 
				
			||||||
 | 
					    final path = p.join(filesStoragePath.absolute.path, expense.localFileName);
 | 
				
			||||||
 | 
					    return File(path).readAsBytes();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Update expense information
 | 
				
			||||||
 | 
					  Future<void> updateExpense(Expense expense, BaseExpenseInfo newInfo) async {
 | 
				
			||||||
 | 
					    final list = await getList();
 | 
				
			||||||
 | 
					    final entry = list.indexWhere((e) => e.id == expense.id);
 | 
				
			||||||
 | 
					    list[entry] = Expense(id: expense.id,
 | 
				
			||||||
 | 
					        label: newInfo.label,
 | 
				
			||||||
 | 
					        cost: newInfo.cost,
 | 
				
			||||||
 | 
					        time: newInfo.timeAsSeconds,
 | 
				
			||||||
 | 
					        mimeType: expense.mimeType);
 | 
				
			||||||
 | 
					    saveList(list);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -16,21 +16,27 @@ class ExpenseEditor extends HookConsumerWidget {
 | 
				
			|||||||
  final Uint8List file;
 | 
					  final Uint8List file;
 | 
				
			||||||
  final Future<void> Function(BaseExpenseInfo) onFinished;
 | 
					  final Future<void> Function(BaseExpenseInfo) onFinished;
 | 
				
			||||||
  final Function()? onRescan;
 | 
					  final Function()? onRescan;
 | 
				
			||||||
 | 
					  final Function()? onDelete;
 | 
				
			||||||
 | 
					  final BaseExpenseInfo? initialData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ExpenseEditor({
 | 
					  const ExpenseEditor({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.file,
 | 
					    required this.file,
 | 
				
			||||||
    required this.onFinished,
 | 
					    required this.onFinished,
 | 
				
			||||||
    required this.onRescan,
 | 
					    this.onRescan,
 | 
				
			||||||
 | 
					    this.onDelete,
 | 
				
			||||||
 | 
					    this.initialData,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final serverConfig = ref.watch(prefsProvider).requireValue.serverConfig()!;
 | 
					    final serverConfig = ref.watch(prefsProvider).requireValue.serverConfig()!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final labelController = useTextEditingController();
 | 
					    final labelController = useTextEditingController(text: initialData?.label);
 | 
				
			||||||
    final costController = useTextEditingController();
 | 
					    final costController = useTextEditingController(
 | 
				
			||||||
    final timeController = useState(DateTime.now());
 | 
					      text: initialData?.cost.toString(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    final timeController = useState(initialData?.time ?? DateTime.now());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final (:pending, :snapshot, :hasError) = useAsyncTask();
 | 
					    final (:pending, :snapshot, :hasError) = useAsyncTask();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -72,16 +78,34 @@ class ExpenseEditor extends HookConsumerWidget {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Delete expense
 | 
				
			||||||
 | 
					    handleDelete() async {
 | 
				
			||||||
 | 
					      if (await confirm(
 | 
				
			||||||
 | 
					            context,
 | 
				
			||||||
 | 
					            content: Text("Do you really want to delete this expense?"),
 | 
				
			||||||
 | 
					          ) &&
 | 
				
			||||||
 | 
					          onDelete != null) {
 | 
				
			||||||
 | 
					        onDelete!();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: Text("Expense info"),
 | 
					        title: Text("Expense info"),
 | 
				
			||||||
        actions: [
 | 
					        actions: [
 | 
				
			||||||
          // Rescan expense
 | 
					          // Rescan expense
 | 
				
			||||||
          IconButton(
 | 
					          onRescan == null
 | 
				
			||||||
            onPressed: onRescan == null ? null : handleRescan,
 | 
					              ? Container()
 | 
				
			||||||
 | 
					              : IconButton(
 | 
				
			||||||
 | 
					                  onPressed: handleRescan,
 | 
				
			||||||
                  icon: Icon(Icons.restart_alt),
 | 
					                  icon: Icon(Icons.restart_alt),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Delete expense
 | 
				
			||||||
 | 
					          onDelete == null
 | 
				
			||||||
 | 
					              ? Container()
 | 
				
			||||||
 | 
					              : IconButton(onPressed: handleDelete, icon: Icon(Icons.delete)),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Submit
 | 
					          // Submit
 | 
				
			||||||
          snapshot.connectionState == ConnectionState.waiting
 | 
					          snapshot.connectionState == ConnectionState.waiting
 | 
				
			||||||
              ? CircularProgressIndicator()
 | 
					              ? CircularProgressIndicator()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								moneymgr_mobile/lib/widgets/loading_scaffold.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								moneymgr_mobile/lib/widgets/loading_scaffold.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LoadingScaffold extends StatelessWidget {
 | 
				
			||||||
 | 
					  final String title;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const LoadingScaffold({super.key, required this.title});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(title: Text(title)),
 | 
				
			||||||
 | 
					      body: Center(child: CircularProgressIndicator()),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user