diff --git a/lib/helpers/database_actions.dart b/lib/helpers/database_actions.dart index f8c215c..4a95e66 100644 --- a/lib/helpers/database_actions.dart +++ b/lib/helpers/database_actions.dart @@ -2,11 +2,13 @@ import 'dart:developer' as dev; import 'dart:io'; import 'package:csv/csv_settings_autodetection.dart'; +import 'package:folio/models/database/scrip.dart'; import 'package:folio/services/database/database.dart'; import 'package:folio/models/database/trade_log.dart'; import 'package:folio/models/database/portfolio.dart'; import 'package:folio/models/stock/stock.dart'; import 'package:folio/state/globals.dart'; +import 'package:folio/views/settings/data/import_scrips_list/parsed_scrips.dart'; import 'package:html/parser.dart' as html; import 'package:sqflite/sqflite.dart'; @@ -16,7 +18,19 @@ import 'package:excel/excel.dart'; import '../models/trade/parsed_file_logs.dart'; +class DataException implements Exception { + final String message; + + DataException(this.message); // Pass your message in constructor. + + @override + String toString() { + return message; + } +} + class DatabaseActions { + static const String delimiter = "; "; static void dummyOnUpdate({int? current, String? message, int? total}) {} static Future getDbPath() async => await Db().getDbPath(); @@ -69,12 +83,7 @@ class DatabaseActions { int qty, double rate, ) async { - String codeCol = ""; - if (exchange == "BSE") { - codeCol = Db.colBSECode; - } else if (exchange == "NSE") { - codeCol = Db.colNSECode; - } + String codeCol = getCodeCol(exchange); var scrips = await Db().getQuery(Db.tblScrips, "$codeCol = ?", [code]); if (scrips.length > 1) { @@ -293,6 +302,48 @@ class DatabaseActions { return sellTrades; } + static Future>> getScripsFromOldCode( + String exchange, String code) async { + var res = await Db().getQuery( + Db.tblScrips, "${getOldCodesCol(exchange)} LIKE ?", ["%$code%"]); + return res; + } + + static Future>> getScripsFromCode( + String exchange, String code) async { + var res = await Db() + .getQuery(Db.tblScrips, "${getCodeCol(exchange)} = ?", [code]); + return res; + } + + static Future>> getScripsFromName( + String name) async { + var res = await Db().getQuery( + Db.tblScrips, "${Db.colName} LIKE ?", ["${getNormalizedName(name)}%"]); + return res; + } + + static Future isOldCodePresent(String exchange, String code) async { + return (await getScripsFromOldCode(exchange, code)).length != 0; + } + + static Future isCodePresent(String exchange, String code) async { + var tuples = await getScripsFromCode(exchange, code); + return tuples.length != 0; + } + + static Future isScripNamePresent(String name) async { + var tuples = await getScripsFromName(name); + return tuples.length != 0; + } + + static Future?> getAllScrips() async { + var scripsTuple = await Db().getOrdered(Db.tblScrips, "${Db.colName} ASC"); + List scripsList = []; + scripsTuple.forEach((row) => scripsList.add(Scrip.fromDbTuple(row))); + return scripsList; + } + static Future setCodesNGetStockID(String? bseCode, String? nseCode, [String? name]) async { List scrips; @@ -430,7 +481,7 @@ class DatabaseActions { date = DateTime.utc(year, month, day); } String? exchange = row[exchangeColID]?.value.toString().trim(); - String? name = row[nameColID]?.value.toString(); + String? name = getNormalizedName(row[nameColID]?.value.toString()); bool? bought = row[boughtColID]?.value.toString() == "B"; int? qty = row[qtyColID] == null ? null @@ -493,14 +544,16 @@ class DatabaseActions { for (var cell in row.querySelectorAll("td")) { switch (headers[colNum]) { case "Date": - date = DateTime.utc( - int.parse(cell.innerHtml - .substring(cell.innerHtml.lastIndexOf('/') + 1)), - int.parse(cell.innerHtml.substring( - cell.innerHtml.indexOf('/') + 1, - cell.innerHtml.lastIndexOf('/'))), - int.parse( - cell.innerHtml.substring(0, cell.innerHtml.indexOf('/')))); + try { + date = DateTime.utc( + int.parse(cell.innerHtml + .substring(cell.innerHtml.lastIndexOf('/') + 1)), + int.parse(cell.innerHtml.substring( + cell.innerHtml.indexOf('/') + 1, + cell.innerHtml.lastIndexOf('/'))), + int.parse(cell.innerHtml + .substring(0, cell.innerHtml.indexOf('/')))); + } catch (e) {} break; case "Exch": exchange = cell.innerHtml.trim(); @@ -517,16 +570,24 @@ class DatabaseActions { .trim(); break; case "Buy Qty": - buyQty = int.parse(cell.innerHtml.trim()); + try { + buyQty = int.parse(cell.innerHtml.trim()); + } catch (e) {} break; case "Sold Qty": - sellQty = int.parse(cell.innerHtml.trim()); + try { + sellQty = int.parse(cell.innerHtml.trim()); + } catch (e) {} break; case "Buy Rate": - buyRate = double.parse(cell.innerHtml.trim()); + try { + buyRate = double.parse(cell.innerHtml.trim()); + } catch (e) {} break; case "Sold Rate": - sellRate = double.parse(cell.innerHtml.trim()); + try { + sellRate = double.parse(cell.innerHtml.trim()); + } catch (e) {} break; default: break; @@ -588,29 +649,31 @@ class DatabaseActions { double? rate; bool? bought; - int i = 0; + int colNum = 0; for (var element in row) { - switch (trades.first[i]) { + switch (trades.first[colNum]) { case "Date": if (element == "null") break; - date = DateTime.utc( - int.parse( - element.substring( - 0, - element.indexOf('-'), + try { + date = DateTime.utc( + int.parse( + element.substring( + 0, + element.indexOf('-'), + ), ), - ), - int.parse( - element.substring( - element.indexOf('-') + 1, - element.lastIndexOf('-'), + int.parse( + element.substring( + element.indexOf('-') + 1, + element.lastIndexOf('-'), + ), ), - ), - int.parse( - element.substring(element.lastIndexOf('-') + 1), - ), - ); + int.parse( + element.substring(element.lastIndexOf('-') + 1), + ), + ); + } catch (e) {} break; case "Exchange": if (element == "null") break; @@ -618,7 +681,7 @@ class DatabaseActions { break; case "Name": if (element == "null") break; - name = element; + name = getNormalizedName(element); break; case "BSE Code": if (element == "null") break; @@ -630,11 +693,15 @@ class DatabaseActions { break; case "Quantity": if (element == "null") break; - qty = int.parse(element.toString()); + try { + qty = int.parse(element.toString()); + } catch (e) {} break; case "Rate": if (element == "null") break; - rate = double.parse(element.toString()); + try { + rate = double.parse(element.toString()); + } catch (e) {} break; case "BUY/SELL": if (element == "null") break; @@ -650,7 +717,7 @@ class DatabaseActions { default: break; } - i++; + colNum++; } switch (exchange) { case "BSE": @@ -767,21 +834,23 @@ class DatabaseActions { fileLog.rate == null) { onUpdate( message: "Invalid logs found!", current: i, total: logs.length); - throw Exception("Invalid logs found!"); + throw DataException("Invalid logs found!"); } - try { - int id = await DatabaseActions.setCodesNGetStockID( - fileLog.bseCode, fileLog.nseCode, fileLog.name); + if (fileLog.stockID == null) { + var res = await DatabaseActions.getScripsFromCode( + fileLog.exchange!, fileLog.code!); - trades.add(TradeLog(fileLog.date!, id, fileLog.code!, fileLog.exchange!, - fileLog.bought!, fileLog.qty!, fileLog.rate!)); - } catch (e) { - onUpdate( - message: "Unknown Error Occurred!", current: i, total: logs.length); - throw e; + if (res.length == 0) + throw DataException( + "Stock of ${fileLog.name} not present in securities list"); + + fileLog.stockID = Scrip.fromDbTuple(res.first).stockID; } + trades.add(TradeLog(fileLog.date!, fileLog.stockID!, fileLog.code!, + fileLog.exchange!, fileLog.bought!, fileLog.qty!, fileLog.rate!)); + i++; } @@ -790,6 +859,159 @@ class DatabaseActions { return trades; } + static Future parseCSVScripsFile( + String exchange, String file, + {void Function({int? current, String? message, int? total}) onUpdate = + dummyOnUpdate}) async { + onUpdate(message: "Parsing Scrips File"); + var detector = FirstOccurrenceSettingsDetector(eols: ['\r\n', '\n']); + List> scrips = + const CsvToListConverter().convert(file, csvSettingsDetector: detector); + + Map scripsMap = {}; + + switch (exchange) { + case "BSE": + int i = 0; + onUpdate( + message: "Parsing BSE Scrips File", + current: i, + total: scrips.length - 1); + for (var row in scrips.skip(1)) { + onUpdate( + message: "Parsing BSE Scrips File", + current: ++i, + total: scrips.length - 1); + + String code = row[0].toString(); + String name = getNormalizedName(row[3].toString())!; + bool isActive = (row[4].toString() == "Active"); + if (scripsMap[name] == null) { + if (isActive) + scripsMap[name] = ParsedScrip(name, code); + else + scripsMap[name] = ParsedScrip(name, null, [code]); + } else { + if (isActive) { + if (scripsMap[name]!.newCode == null) + scripsMap[name]!.newCode = code; + else { + scripsMap[name]!.oldCodes.add(code); + // onUpdate( + // message: "Two codes ${scripsMap[name]!.newCode} & $code for" + // " the same stock $name found!", + // current: i, + // total: null); + // throw DataException("Two codes ${scripsMap[name]!.newCode} & $code for" + // " the same stock $name found!"); + } + } else { + scripsMap[name]!.oldCodes.add(code); + } + } + } + + break; + case "NSE": + int i = 0; + onUpdate( + message: "Parsing NSE Scrips File", + current: i, + total: scrips.length - 1); + for (var row in scrips.skip(1)) { + onUpdate( + message: "Parsing NSE Scrips File", + current: ++i, + total: scrips.length - 1); + + String code = row[0].toString(); + String name = getNormalizedName(row[1].toString())!; + scripsMap[name] = ParsedScrip(name, code); + } + + break; + } + + ParsedScripsList scripsList = ParsedScripsList(exchange); + int i = 0, total = scripsMap.values.length; + onUpdate(message: "Checking Parsed Scrips", current: i, total: total); + + for (var scrip in scripsMap.values) { + onUpdate(message: "Checking Parsed Scrips", current: ++i, total: total); + + ParsedScrip checkedScrip = ParsedScrip.from(scrip); + + for (var oldCode in scrip.oldCodes) { + if (await isOldCodePresent(exchange, oldCode) || + await isCodePresent(exchange, oldCode)) { + checkedScrip.oldCodes.remove(oldCode); + } + } + + if (checkedScrip.oldCodes.length == 0) { + if (scrip.newCode == null || + await isCodePresent(exchange, scrip.newCode!)) { + continue; + } + } + + if (scrip.newCode != null) { + var newCodeScrip = await getScripsFromOldCode(exchange, scrip.newCode!); + if (newCodeScrip.length != 0) { + checkedScrip.newCode = newCodeScrip.first[getCodeCol(exchange)]; + } + } + + scripsList.addNew(checkedScrip); + } + + onUpdate(message: "Done", current: total, total: total); + + return scripsList; + } + + static addScrips(ParsedScripsList scripsList, + {void Function({int? current, String? message, int? total}) onUpdate = + dummyOnUpdate}) async { + var exchange = scripsList.exchange, total = scripsList.newScrips.length; + + int i = 0; + onUpdate(message: "Importing Scrips", current: i, total: total); + + for (var scrip in scripsList.newScrips) { + onUpdate(message: "Importing Scrips", current: ++i, total: total); + var scripTuple = await getScripsFromName(scrip.name); + if (scripTuple.length == 0) { + Scrip newScrip = Scrip(getNormalizedName(scrip.name)!); + newScrip.setCode(exchange, scrip.newCode); + newScrip.setOldCodes(exchange, scrip.oldCodes); + Db().insert(Db.tblScrips, newScrip.toDbTuple()); + } else { + Scrip existingScrip = Scrip.fromDbTuple(scripTuple.first); + String? existingCode = existingScrip.code(exchange); + List existingOldCodes = existingScrip.oldCodes(exchange); + for (var oldCode in scrip.oldCodes) { + if (!existingOldCodes.contains(oldCode)) { + existingOldCodes.add(oldCode); + } + } + + if (scrip.newCode == null && existingCode != null) { + existingOldCodes.add(existingCode); + } + + Db().updateConditionally( + Db.tblScrips, + { + getCodeCol(exchange): scrip.newCode, + getOldCodesCol(exchange): existingOldCodes.join(delimiter) + }, + "${Db.colName} = ?", + [getNormalizedName(scrip.name)]); + } + } + } + static Future getTradesCSV() async { List tradeLogs = await Db().getRawQuery("" "SELECT " @@ -889,17 +1111,40 @@ class DatabaseActions { } } - static Future setScripName( - String exchange, String? name, String code) async { - String codeCol = ""; - if (exchange == "BSE") { - codeCol = Db.colBSECode; - } else if (exchange == "NSE") { - codeCol = Db.colNSECode; + static Future getScripCode(String name, String exchange) async { + var scrips = await Db() + .getQuery(Db.tblScrips, "${Db.colName} = ?", [getNormalizedName(name)]); + return scrips.first[getCodeCol(exchange)]; + } + + static String getCodeCol(String exchange) { + switch (exchange) { + case "BSE": + return Db.colBSECode; + case "NSE": + return Db.colNSECode; } + return ""; + } + + static String getOldCodesCol(String exchange) { + switch (exchange) { + case "BSE": + return Db.colOldBSECodes; + case "NSE": + return Db.colOldNSECodes; + } + + return ""; + } + + static Future setScripName( + String exchange, String name, String code) async { + String codeCol = getCodeCol(exchange); + return await Db().updateConditionally(Db.tblScrips, - {Db.colName: name?.toUpperCase()}, '$codeCol = ?', [code]); + {Db.colName: getNormalizedName(name)}, '$codeCol = ?', [code]); } static Future updatePinned( @@ -922,15 +1167,7 @@ class DatabaseActions { static Future addTracked( String code, String exchange, bool pinned) async { - String codeCol = ""; - switch (exchange) { - case "BSE": - codeCol = Db.colBSECode; - break; - case "NSE": - codeCol = Db.colNSECode; - break; - } + String codeCol = getCodeCol(exchange); return await Db().transact((txn) async { // Check if Scrips has the newCode already @@ -955,15 +1192,7 @@ class DatabaseActions { static Future updateCode( String oldCode, String exchange, String newCode) async { - String codeCol = ""; - switch (exchange) { - case "BSE": - codeCol = Db.colBSECode; - break; - case "NSE": - codeCol = Db.colNSECode; - break; - } + String codeCol = getCodeCol(exchange); return await Db().transact((txn) async { // Check if Scrips has the newCode already @@ -999,4 +1228,108 @@ class DatabaseActions { static void deleteDbThenInit() { Db().deleteDbThenInit(); } + + static Future resolveInvalidLogs(ParsedFileLogs logs, + {void Function({int? current, String? message, int? total}) onUpdate = + dummyOnUpdate}) async { + int i = 0; + onUpdate( + message: "Trying to fix invalid logs", + current: i, + total: logs.invalidLogs.length); + List removeIndexes = []; + for (var log in logs.invalidLogs) { + onUpdate( + message: "Trying to fix invalid logs", + current: i + 1, + total: logs.invalidLogs.length); + // Find code if not present + if (log.code == null && log.name != null && log.exchange != null) { + var scripsTuple = await getScripsFromName(log.name!); + if (scripsTuple.length != 0) { + var scrip = Scrip.fromDbTuple(scripsTuple.first); + log.bseCode = scrip.bseCode; + log.nseCode = scrip.nseCode; + log.stockID = scrip.stockID; + } + } + + if (log.date != null && + log.code != null && + log.exchange != null && + log.bought != null && + log.qty != null && + log.rate != null) { + logs.validLogs.add(log); + removeIndexes.add(i); + } + + i++; + } + + for (var index in removeIndexes.reversed) { + logs.invalidLogs.removeAt(index); + } + + return logs; + } + + /// Normalize stock names to prevent string matching problems + static String? getNormalizedName(String? name) { + if (name == null) return null; + name = name.toUpperCase().trim(); + + // BSE List of scrips contain these unnecessary characters at the end + if (name.endsWith("-\$")) { + name = name.substring(0, name.length - 2); + } + + // Shorten words to common acronyms + name = " $name "; + name = name + .replaceAll(" CO.LTD. ", " CO. LTD. ") + .replaceAll(" ENGG.LTD. ", " ENGG. LTD. ") + .replaceAll(" ENGINEERING ", " ENGG. ") + .replaceAll(" LIMITED ", " LTD. ") + .replaceAll(" LTD ", " LTD. ") + .replaceAll(" COMPANY ", " CO. ") + .replaceAll(" ENTERPRISES ", " ENT. ") + .replaceAll(" ENT ", " ENT. ") + .replaceAll(" CORPORATION ", " CORP. ") + .replaceAll(" CORP ", " CORP. "); + + return name.trim(); + } + + static void editScripName(int stockID, String name) async { + var tuples = await getScripsFromName(name); + if (tuples.length == 0) { + await Db().updateConditionally( + Db.tblScrips, {Db.colName: name}, "${Db.colRowID} = ?", [stockID]); + } else if (tuples.first[Db.colRowID] != stockID) { + throw DataException("Scrip name already present!"); + } + } + + static Future deleteScrip(int stockID) async { + var portfolioLogs = + await Db().getQuery(Db.tblPortfolio, "${Db.colStockID} = ?", [stockID]); + if (portfolioLogs.isNotEmpty) + throw DataException( + "Can't delete security as it exists in your Portfolio"); + + var tradeLogs = + await Db().getQuery(Db.tblTradeLog, "${Db.colStockID} = ?", [stockID]); + if (tradeLogs.isNotEmpty) + throw DataException( + "Can't delete security as it exists in your TradeLogs"); + + var trackedTuple = + await Db().getQuery(Db.tblTracked, "${Db.colStockID} = ?", [stockID]); + if (trackedTuple.isNotEmpty) + throw DataException( + "Can't delete security as it exists in your Tracked list"); + + await Db().deleteQuery(Db.tblScrips, "${Db.colRowID} = ?", [stockID]); + } } diff --git a/lib/helpers/stock_repository.dart b/lib/helpers/stock_repository.dart index 95563f3..f6fe604 100644 --- a/lib/helpers/stock_repository.dart +++ b/lib/helpers/stock_repository.dart @@ -15,8 +15,10 @@ class StockRepository { static Future getName(String code, String exchange) async { String? name = await QueryAPI.getName(exchange: exchange, code: code); - - DatabaseActions.setScripName(exchange, name, code); + + if (name != null) + DatabaseActions.setScripName(exchange, name, code); + return name?.toUpperCase(); } diff --git a/lib/models/database/scrip.dart b/lib/models/database/scrip.dart new file mode 100644 index 0000000..850ad78 --- /dev/null +++ b/lib/models/database/scrip.dart @@ -0,0 +1,82 @@ +import 'package:folio/helpers/database_actions.dart'; +import 'package:folio/services/database/database.dart'; + +class Scrip { + int stockID = -1; + late String name; + String? bseCode; + String? nseCode; + List oldBSECodes = []; + List oldNSECodes = []; + + Scrip(this.name); + + Scrip.fromDbTuple(Map tuple) { + stockID = tuple[Db.colRowID]; + name = tuple[Db.colName]; + bseCode = tuple[Db.colBSECode]; + nseCode = tuple[Db.colNSECode]; + oldBSECodes = + tuple[Db.colOldBSECodes]?.split(DatabaseActions.delimiter) ?? []; + oldNSECodes = + tuple[Db.colOldNSECodes]?.split(DatabaseActions.delimiter) ?? []; + oldBSECodes.remove(""); + oldNSECodes.remove(""); + } + + Map toDbTuple() { + return { + Db.colName: name, + Db.colBSECode: bseCode, + Db.colNSECode: nseCode, + Db.colOldBSECodes: oldBSECodes.join(DatabaseActions.delimiter), + Db.colOldNSECodes: oldNSECodes.join(DatabaseActions.delimiter), + }; + } + + String? code(String exchange) { + switch (exchange) { + case "BSE": + return bseCode; + case "NSE": + return nseCode; + } + throw Exception("Wrong exchange passed to code() of ${this.toString()}"); + } + + List oldCodes(String exchange) { + switch (exchange) { + case "BSE": + return oldBSECodes; + case "NSE": + return oldNSECodes; + } + throw Exception( + "Wrong exchange passed to oldCodes() of ${this.toString()}"); + } + + void setCode(String exchange, String? code) { + switch (exchange) { + case "BSE": + bseCode = code; + return; + case "NSE": + nseCode = code; + return; + } + throw Exception("Wrong exchange passed to setCode() of ${this.toString()}"); + } + + void setOldCodes(String exchange, List oldCodes) { + switch (exchange) { + case "BSE": + oldBSECodes = oldCodes; + return; + case "NSE": + oldNSECodes = oldCodes; + return; + } + throw Exception( + "Wrong exchange passed to setOldCodes() of ${this.toString()}"); + } +} diff --git a/lib/models/trade/parsed_file_logs.dart b/lib/models/trade/parsed_file_logs.dart index 6f80d08..51eec44 100644 --- a/lib/models/trade/parsed_file_logs.dart +++ b/lib/models/trade/parsed_file_logs.dart @@ -1,5 +1,6 @@ class FileLog { DateTime? date; + int? stockID; String? name; String? bseCode; String? nseCode; diff --git a/lib/services/database/database.dart b/lib/services/database/database.dart index 4350169..b3dc5a3 100644 --- a/lib/services/database/database.dart +++ b/lib/services/database/database.dart @@ -20,6 +20,8 @@ class Db { static String colCode = 'code'; static String colBSECode = 'bse_code'; static String colNSECode = 'nse_code'; + static String colOldBSECodes = 'old_bse_codes'; + static String colOldNSECodes = 'old_nse_codes'; static String colExch = 'exchange'; static String colName = 'name'; static String colKey = 'key'; @@ -80,7 +82,9 @@ class Db { '$colRowID INTEGER PRIMARY KEY, ' '$colBSECode TEXT UNIQUE, ' '$colNSECode TEXT UNIQUE, ' - '$colName TEXT' + '$colOldBSECodes TEXT, ' + '$colOldNSECodes TEXT, ' + '$colName TEXT UNIQUE' ')'); } @@ -103,7 +107,7 @@ class Db { return count; } - Future> getAllTuples(String table) async { + Future>> getAllTuples(String table) async { var dbClient = await db; List> result = await dbClient.query(table); diff --git a/lib/views/settings/data/data.dart b/lib/views/settings/data/data.dart index bc0a54a..d27f103 100644 --- a/lib/views/settings/data/data.dart +++ b/lib/views/settings/data/data.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:folio/views/settings/data/scrips_list.dart'; import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; @@ -93,11 +94,14 @@ class _ImportAreaState extends State { Icons.folder_open_outlined, color: Theme.of(context).colorScheme.onPrimary, ), - onTap: !_isButtonEnabled ? null : () { - Navigator.push(context, MaterialPageRoute(builder: (context) { - return ImportFileRoute(); - })); - }, + onTap: !_isButtonEnabled + ? null + : () { + Navigator.push(context, + MaterialPageRoute(builder: (context) { + return ImportFileRoute(); + })); + }, ), ListTile( title: Text("Track a stock"), @@ -147,6 +151,29 @@ class _ImportAreaState extends State { : null, ), Divider(), + Text( + "Securities", + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ListTile( + title: Text("Show Securities"), + trailing: Icon( + Icons.view_list_outlined, + color: Theme.of(context).colorScheme.onPrimary, + ), + onTap: !_isButtonEnabled + ? null + : () { + Navigator.push(context, + MaterialPageRoute(builder: (context) { + return ShowSecuritiesRoute(); + })); + }, + ), + Divider(), Text( "Export", style: Theme.of(context) @@ -185,56 +212,7 @@ class _ImportAreaState extends State { ), title: Text("Delete Database"), onTap: () async { - String? result = await showDialog( - context: context, - builder: (_) => AlertDialog( - title: Center(child: Text("WARNING")), - content: Text( - "This will delete the database. Proceed only if you know what you are doing.", - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.justify, - ), - actions: [ - TextButton( - style: TextButton.styleFrom( - minimumSize: Size(88, 36), - padding: EdgeInsets.symmetric(horizontal: 16), - shape: const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(5)), - )), - onPressed: () { - Navigator.pop(context, "Delete"); - }, - child: Text("Delete"), - ), - SizedBox( - width: 5, - ), - ElevatedButton( - style: TextButton.styleFrom( - foregroundColor: - Theme.of(context).colorScheme.background, - minimumSize: Size(88, 36), - padding: EdgeInsets.symmetric(horizontal: 16), - shape: const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(5)), - ), - ), - onPressed: () { - Navigator.pop(context, "Cancel"); - }, - child: Text("Cancel"), - ), - ], - actionsPadding: - EdgeInsets.symmetric(horizontal: 10, vertical: 10), - ), - ); - if (result == "Delete") { - DatabaseActions.deleteDbThenInit(); - } + await deleteDatabase(context); }, ) ], @@ -292,7 +270,58 @@ class _ImportAreaState extends State { ); } - + Future deleteDatabase(BuildContext context) async { + String? result = await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Center(child: Text("WARNING")), + content: Text( + "This will delete the database. Proceed only if you know what you are doing.", + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.justify, + ), + actions: [ + TextButton( + style: TextButton.styleFrom( + minimumSize: Size(88, 36), + padding: EdgeInsets.symmetric(horizontal: 16), + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(5)), + )), + onPressed: () { + Navigator.pop(context, "Delete"); + }, + child: Text("Delete"), + ), + SizedBox( + width: 5, + ), + ElevatedButton( + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.background, + minimumSize: Size(88, 36), + padding: EdgeInsets.symmetric(horizontal: 16), + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(5)), + ), + ), + onPressed: () { + Navigator.pop(context, "Cancel"); + }, + child: Text("Cancel"), + ), + ], + actionsPadding: + EdgeInsets.symmetric(horizontal: 10, vertical: 10), + ), + ); + if (result == "Delete") { + DatabaseActions.deleteDbThenInit(); + } + } void exportLogs() async { setState(() { diff --git a/lib/views/settings/data/import_file/file_log_tile.dart b/lib/views/settings/data/import_file/file_log_tile.dart index a5d21fc..5b1a0bf 100644 --- a/lib/views/settings/data/import_file/file_log_tile.dart +++ b/lib/views/settings/data/import_file/file_log_tile.dart @@ -405,7 +405,7 @@ class _InvalidFileLogTile extends State ), )), enabled: - widget._log.date == null && !widget.isLogValid, + widget._log.qty == null && !widget.isLogValid, cursorColor: Theme.of(context).colorScheme.secondary, style: Theme.of(context).textTheme.bodyLarge, keyboardType: TextInputType.number, @@ -452,7 +452,7 @@ class _InvalidFileLogTile extends State ), )), enabled: - widget._log.date == null && !widget.isLogValid, + widget._log.rate == null && !widget.isLogValid, cursorColor: Theme.of(context).colorScheme.secondary, style: Theme.of(context).textTheme.bodyLarge, keyboardType: diff --git a/lib/views/settings/data/import_file/get_missing_codes.dart b/lib/views/settings/data/import_file/get_missing_codes.dart new file mode 100644 index 0000000..867f937 --- /dev/null +++ b/lib/views/settings/data/import_file/get_missing_codes.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:folio/models/trade/parsed_file_logs.dart'; +import 'package:folio/views/settings/data/import_file/correct_invalid_logs.dart'; +import 'package:folio/views/settings/data/import_file/name_code_tile.dart'; + +class GetMissingCodes extends StatefulWidget { + final ParsedFileLogs _logs; + + GetMissingCodes(this._logs); + + @override + _GetMissingCodesState createState() => _GetMissingCodesState(); +} + +class _GetMissingCodesState extends State { + late List correctedCodes; + late Map nameIndexMap; + late Future> codesFuture; + late List isCorrected; + late bool isLoading; + + @override + void initState() { + super.initState(); + isLoading = true; + correctedCodes = []; + nameIndexMap = {}; + codesFuture = getStocksWithInvalidCodes(widget._logs.invalidLogs); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: Theme.of(context).colorScheme.background, + title: Text("Correct Missing Fields"), + centerTitle: true, + actions: [ + IconButton( + icon: Icon(correctedCodes.length == 0 ? Icons.arrow_forward : Icons.check), + onPressed: isLoading ? null : () async { + await nextPage(context); + }, + ) + ], + ), + body: CustomScrollView( + slivers: [ + FutureBuilder( + future: codesFuture, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return SliverFillRemaining( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: LinearProgressIndicator(), + ), + ), + ); + } + if (snapshot.hasError) { + return SliverFillRemaining( + child: Center( + child: Text( + "Error Occurred", + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ); + } + if ((snapshot.data?.length ?? 0) == 0) { + return SliverFillRemaining( + child: Center( + child: Text( + "All codes ok. Go to next page!", + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ); + } else { + return SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return NameCodeTile( + index: index, + nameCode: snapshot.data![index], + updateCode: updateCode, + ); + }, + childCount: (snapshot.data?.length ?? 0), + ), + ); + } + }, + ), + ], + ), + bottomNavigationBar: BottomAppBar( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Total: ${correctedCodes.length}", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + ); + } + + Future nextPage(BuildContext context) async { + if (isCorrected.contains(false)) { + await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text("Please provide all required codes"), + actions: [ + TextButton( + child: Text("OK"), + onPressed: () async => Navigator.pop(context), + ) + ], + ); + }, + ); + } else { + validateLogs(); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => CorrectInvalidLogs(widget._logs), + ), + ); + } + } + + void validateLogs() { + List removeIndices = []; + int i = 0; + for (var log in widget._logs.invalidLogs) { + i++; + var index = nameIndexMap[log.name]; + + if (index == null) continue; + + log.bseCode = correctedCodes[index].bseCode; + log.nseCode = correctedCodes[index].nseCode; + if (log.exchange != null && + log.bought != null && + log.date != null && + log.code != null) { + widget._logs.validLogs.add(log); + removeIndices.add(i - 1); + } + } + + for (var index in removeIndices.reversed) + widget._logs.invalidLogs.removeAt(index); + } + + void updateCode(int index, String? bseCode, String? nseCode) { + correctedCodes[index].bseCode = bseCode; + correctedCodes[index].nseCode = nseCode; + + if (correctedCodes[index].isCorrect()) + isCorrected[index] = true; + else + isCorrected[index] = false; + } + + Future> getStocksWithInvalidCodes( + List invalidLogs) async { + for (var log in invalidLogs) { + if (log.code == null && log.name != null) { + var index = nameIndexMap[log.name]; + if (index == null) { + nameIndexMap[log.name!] = correctedCodes.length; + correctedCodes.add(NameCode(log.name!)); + correctedCodes.last.isBSECodeRequired = log.isBSECodeRequired; + correctedCodes.last.isNSECodeRequired = log.isNSECodeRequired; + } else { + correctedCodes[index].isBSECodeRequired = + correctedCodes[index].isBSECodeRequired || log.isBSECodeRequired; + correctedCodes[index].isNSECodeRequired = + correctedCodes[index].isNSECodeRequired || log.isNSECodeRequired; + } + } + } + + isCorrected = List.filled(correctedCodes.length, false); + + setState(() { + isLoading = false; + }); + return correctedCodes; + } +} + +class NameCode { + late String name; + String? bseCode; + String? nseCode; + bool isBSECodeRequired = false, isNSECodeRequired = false; + + NameCode(this.name, {this.bseCode, this.nseCode}); + + bool isCorrect() { + if (isBSECodeRequired && isNSECodeRequired) + return bseCode != null && nseCode != null; + if (isBSECodeRequired) return bseCode != null; + if (isNSECodeRequired) return nseCode != null; + return true; + } +} diff --git a/lib/views/settings/data/import_file/import_file.dart b/lib/views/settings/data/import_file/import_file.dart index e9cfe00..56e8570 100644 --- a/lib/views/settings/data/import_file/import_file.dart +++ b/lib/views/settings/data/import_file/import_file.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:folio/views/settings/data/import_file/get_missing_codes.dart'; import 'package:folio/views/settings/data/import_file/show_valid_logs.dart'; import '../../../../helpers/database_actions.dart'; import '../../../../models/trade/parsed_file_logs.dart'; -import 'correct_invalid_logs.dart'; class ImportFileRoute extends StatefulWidget { @override @@ -227,8 +227,9 @@ class _ImportFileRouteState extends State { updateProgress(); return; } + + logs = await DatabaseActions.resolveInvalidLogs(logs, onUpdate: updateProgress); } catch (e) { - // FIXME: Send error to user updateProgress(message: e.toString(), current: 0, total: 1); setState(() { _isButtonEnabled = true; @@ -240,7 +241,7 @@ class _ImportFileRouteState extends State { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) { - return CorrectInvalidLogs(logs); + return GetMissingCodes(logs); }), ); } else { diff --git a/lib/views/settings/data/import_file/name_code_tile.dart b/lib/views/settings/data/import_file/name_code_tile.dart new file mode 100644 index 0000000..f4baaa1 --- /dev/null +++ b/lib/views/settings/data/import_file/name_code_tile.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:folio/views/settings/data/import_file/get_missing_codes.dart'; + +class NameCodeTile extends StatefulWidget { + final int index; + final NameCode nameCode; + final void Function(int, String?, String?) updateCode; + + NameCodeTile({ + required this.index, + required this.nameCode, + required this.updateCode, + }); + + @override + State createState() => _NameCodeTileState(); +} + +class _NameCodeTileState extends State + with AutomaticKeepAliveClientMixin { + late TextEditingController _bseCodeCtl; + late TextEditingController _nseCodeCtl; + final _formKey = GlobalKey(); + String? bseCode, nseCode; + + @override + void initState() { + super.initState(); + bseCode = widget.nameCode.bseCode; + nseCode = widget.nameCode.nseCode; + _bseCodeCtl = TextEditingController(text: bseCode); + _nseCodeCtl = TextEditingController(text: nseCode); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + return Card( + color: Theme.of(context).colorScheme.background, + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + child: InkWell( + borderRadius: BorderRadius.circular(20), + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 10, horizontal: 5), + child: Center( + child: Text( + widget.nameCode.name, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 1, + child: TextFormField( + decoration: InputDecoration( + labelText: "NSE Code", + contentPadding: EdgeInsets.symmetric( + horizontal: 15, + ), + disabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + width: 2, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: widget.nameCode.isNSECodeRequired + ? Colors.red + : Colors.grey, + width: widget.nameCode.isNSECodeRequired ? 2 : 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: widget.nameCode.isNSECodeRequired + ? Colors.red + : Colors.black, + width: 2, + ), + ), + ), + enabled: widget.nameCode.nseCode == null, + cursorColor: Theme.of(context).colorScheme.secondary, + style: Theme.of(context).textTheme.bodyLarge, + keyboardType: TextInputType.text, + controller: _nseCodeCtl, + validator: (value) { + if (value == null || value.isEmpty) { + if (widget.nameCode.isNSECodeRequired) return 'Required'; + return null; + } + nseCode = value; + updateLog(); + return null; + }, + ), + ), + SizedBox( + width: 10, + ), + Expanded( + flex: 1, + child: TextFormField( + decoration: InputDecoration( + labelText: "BSE Code", + contentPadding: EdgeInsets.symmetric( + horizontal: 15, + vertical: 0, + ), + disabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + width: 2, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: widget.nameCode.isBSECodeRequired + ? Colors.red + : Colors.grey, + width: widget.nameCode.isBSECodeRequired ? 2 : 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: widget.nameCode.isBSECodeRequired + ? Colors.red + : Colors.black, + width: 2, + ), + ), + ), + enabled: widget.nameCode.bseCode == null, + cursorColor: Theme.of(context).colorScheme.secondary, + style: Theme.of(context).textTheme.bodyLarge, + keyboardType: TextInputType.text, + controller: _bseCodeCtl, + validator: (value) { + if ((value == null || value.isEmpty) && + widget.nameCode.isBSECodeRequired) { + return 'Required'; + } + bseCode = value; + updateLog(); + return null; + }, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + void updateLog() { + widget.updateCode(widget.index, bseCode, nseCode); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/views/settings/data/import_scrips_list/import_scrips.dart b/lib/views/settings/data/import_scrips_list/import_scrips.dart new file mode 100644 index 0000000..74dd974 --- /dev/null +++ b/lib/views/settings/data/import_scrips_list/import_scrips.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:folio/helpers/database_actions.dart'; +import 'package:folio/views/settings/data/import_scrips_list/parsed_scrips.dart'; + +class ImportScrips extends StatefulWidget { + final ParsedScripsList _scripsList; + + ImportScrips(this._scripsList); + + @override + _ImportScripsState createState() => _ImportScripsState(); +} + +class _ImportScripsState extends State { + late bool isImporting; + String? _message; + int? _current; + int? _total; + late Future tradesFuture; + + @override + void initState() { + super.initState(); + isImporting = true; + tradesFuture = addScrips(); + } + + Future addScrips() async { + await DatabaseActions.addScrips(widget._scripsList, + onUpdate: updateProgress); + + setState(() { + isImporting = false; + }); + } + + void updateProgress({String? message, int? current, int? total}) { + setState(() { + _message = message; + _current = current; + _total = total; + }); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => !isImporting, + child: Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: Theme.of(context).colorScheme.background, + title: isImporting ? Text("Importing") : Text("Imported"), + centerTitle: true, + automaticallyImplyLeading: !isImporting, + ), + body: FutureBuilder( + future: tradesFuture, + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_message ?? ""), + Text(_total == null ? "" : "${_current ?? "?"}/$_total"), + Padding( + padding: + EdgeInsets.symmetric(vertical: 10, horizontal: 20), + child: _message == null + ? null + : LinearProgressIndicator( + value: _total != null && + _current != null && + _total != 0 + ? _current! / _total! + : null, + ), + ) + ], + ), + ); + } + isImporting = false; + if (snapshot.hasError) { + return Center( + child: Text(_message ?? "Error occurred during import of logs"), + ); + } + return Center( + child: Text( + "Scrips successfully imported", + style: Theme.of(context).textTheme.headlineMedium, + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/views/settings/data/import_scrips_list/parsed_scrips.dart b/lib/views/settings/data/import_scrips_list/parsed_scrips.dart new file mode 100644 index 0000000..4bfc194 --- /dev/null +++ b/lib/views/settings/data/import_scrips_list/parsed_scrips.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; + +class ParsedScrip { + String name; + String? newCode; + List oldCodes; + + ParsedScrip(this.name, this.newCode, [List? oldCodes]) + : oldCodes = oldCodes ?? []; + + ParsedScrip.from(ParsedScrip scrip) + : name = scrip.name, newCode = scrip.newCode, oldCodes = [] { + for (var oldCode in scrip.oldCodes){ + oldCodes.add(oldCode); + } + } +} + +class ParsedScripsList { + final String exchange; + List newScrips = []; + + ParsedScripsList(this.exchange); + + Future addNew(ParsedScrip scrip) async { + newScrips.add(scrip); + } +} + +class ParsedScripTile extends StatelessWidget { + final ParsedScrip _scrip; + + ParsedScripTile(this._scrip); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 3, + child: Center(child: Text("Name ")), + ), + SizedBox( + width: 10, + ), + Expanded( + flex: 2, + child: Center(child: Text("Code ")), + ) + ], + ), + Row( + children: [ + Expanded( + flex: 3, + child: Center( + child: Text( + _scrip.name, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + ), + SizedBox( + width: 10, + ), + Expanded( + flex: 2, + child: Center( + child: _scrip.newCode == null + ? Text("INACTIVE", + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.w100),) + : CodeChip( + code: _scrip.newCode!, + ), + ), + ), + ], + ), + _scrip.oldCodes.length == 0 + ? SizedBox() + : Column( + children: [ + Divider(thickness: 2,), + Padding( + padding: const EdgeInsets.only( + left: 8.0, top: 5, right: 8.0, bottom: 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(_scrip.newCode == null ? "Old Codes: " : "Replaces: "), + Expanded( + child: Wrap( + spacing: 5, + alignment: WrapAlignment.end, + children: _scrip.oldCodes + .map((code) => CodeChip(code: code)) + .toList(), + ), + ), + ], + ), + ), + ], + ) + ], + ), + ), + ), + ); + } +} + +class CodeChip extends StatelessWidget { + const CodeChip({ + required String code, + }) : _code = code; + + final String _code; + + @override + Widget build(BuildContext context) { + return Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + color: Theme.of(context).primaryColor, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 2), + child: Text( + _code, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).cardColor), + ), + ), + ); + } +} diff --git a/lib/views/settings/data/import_scrips_list/select_scrips_file.dart b/lib/views/settings/data/import_scrips_list/select_scrips_file.dart new file mode 100644 index 0000000..4742294 --- /dev/null +++ b/lib/views/settings/data/import_scrips_list/select_scrips_file.dart @@ -0,0 +1,321 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:folio/views/settings/data/import_scrips_list/parsed_scrips.dart'; + +import 'package:folio/helpers/database_actions.dart'; +import 'package:folio/views/settings/data/import_scrips_list/show_scrips.dart'; + +class ImportScripsFileRoute extends StatefulWidget { + @override + State createState() => _ImportScripsFileRouteState(); +} + +class _ImportScripsFileRouteState extends State { + PlatformFile? pickedFile; + bool _isButtonEnabled = true; + String? _message; + int? _current; + int? _total; + List _isExchangeSelected = [true, false]; + List _exchanges = ["BSE", "NSE"]; + + void updateProgress({String? message, int? current, int? total}) { + setState(() { + _message = message; + _current = current; + _total = total; + }); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => _isButtonEnabled, + child: Scaffold( + appBar: AppBar( + title: Center(child: Text("Select File")), + backgroundColor: Theme.of(context).colorScheme.background, + elevation: 0, + actions: [ + IconButton( + icon: Icon( + Icons.circle_outlined, + color: Theme.of(context).colorScheme.background, + ), + onPressed: null, + ), + ], + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + color: Theme.of(context).colorScheme.background, + margin: EdgeInsets.all(10), + elevation: 2, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: ToggleButtons( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "BSE", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith( + color: _isExchangeSelected[0] + ? Theme.of(context) + .colorScheme + .background + : Theme.of(context) + .colorScheme + .onPrimary, + ), + ), + ), + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "NSE", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith( + color: _isExchangeSelected[1] + ? Theme.of(context) + .colorScheme + .background + : Theme.of(context) + .colorScheme + .onPrimary, + ), + ), + ), + ], + onPressed: (int index) { + setState(() { + for (int buttonIndex = 0; + buttonIndex < _isExchangeSelected.length; + buttonIndex++) { + if (buttonIndex == index) { + _isExchangeSelected[buttonIndex] = true; + } else { + _isExchangeSelected[buttonIndex] = false; + } + } + }); + }, + isSelected: _isExchangeSelected, + selectedColor: _isExchangeSelected[0] + ? Colors.lightBlue + : Colors.orange, + fillColor: _isExchangeSelected[0] + ? Colors.lightBlue + : Colors.orange, + borderRadius: BorderRadius.circular(40), + ), + ), + Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1), + borderRadius: BorderRadius.circular(4), + ), + color: Theme.of(context).colorScheme.background, + ), + child: InkWell( + onTap: _isButtonEnabled ? pickFile : null, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 9, + child: Text( + pickedFile?.path == null + ? "" + : pickedFile!.path!.substring( + pickedFile!.path!.lastIndexOf("/") + + 1), + softWrap: true, + ), + ), + Expanded( + child: IconButton( + icon: Icon(Icons.file_open), + onPressed: + _isButtonEnabled ? pickFile : null, + ), + ) + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: ElevatedButton( + onPressed: + _isButtonEnabled && pickedFile?.path != null + ? importScrips + : null, + child: Text( + "Import", + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: Colors.white), + ), + ), + ), + ], + ), + ), + ), + ), + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_message ?? ""), + Text(_total == null ? "" : "${_current ?? "?"}/$_total"), + Padding( + padding: + EdgeInsets.symmetric(vertical: 10, horizontal: 20), + child: _message == null + ? null + : LinearProgressIndicator( + value: _total != null && + _current != null && + _total != 0 + ? _current! / _total! + : null, + ), + ) + ], + ), + ), + ), + Expanded( + flex: 2, + child: SizedBox(), + ), + ], + ), + ), + ); + } + + void pickFile() async { + setState(() { + _isButtonEnabled = false; + }); + + FilePickerResult? result = + await FilePicker.platform.pickFiles(type: FileType.any); + + if (result == null || result.files.first.path == null) { + setState(() { + pickedFile = null; + _isButtonEnabled = true; + }); + + return; + } + + setState(() { + pickedFile = result.files.first; + _isButtonEnabled = true; + }); + + return; + } + + String getSelectedExchange() { + for (int i = 0; i < _isExchangeSelected.length; i++) { + if (_isExchangeSelected[i]) { + return _exchanges[i]; + } + } + return ""; + } + + void importScrips() async { + if (pickedFile == null || pickedFile?.path == null) { + return; + } + + setState(() { + _isButtonEnabled = false; + }); + updateProgress(message: "Importing File"); + + ParsedScripsList scripsList; + String filePath = pickedFile!.path!; + + try { + switch (pickedFile?.extension) { + case "csv": + String file = ""; + try { + file = File(filePath).readAsStringSync(); + } catch (e) { + log("data.importLogs() => Error in reading file\n " + e.toString()); + updateProgress( + message: "Error in reading file", current: 0, total: 1); + setState(() { + _isButtonEnabled = true; + }); + return; + } + updateProgress(message: "Parsing CSV File"); + scripsList = await DatabaseActions.parseCSVScripsFile( + getSelectedExchange(), file, + onUpdate: updateProgress); + break; + default: + setState(() { + _isButtonEnabled = true; + }); + updateProgress(); + return; + } + } catch (e) { + updateProgress(message: e.toString(), current: 0, total: 1); + setState(() { + _isButtonEnabled = true; + }); + return; + } + + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) { + return ShowScrips(scripsList); + }), + ); + + setState(() { + _isButtonEnabled = true; + }); + } +} diff --git a/lib/views/settings/data/import_scrips_list/show_scrips.dart b/lib/views/settings/data/import_scrips_list/show_scrips.dart new file mode 100644 index 0000000..4b83be1 --- /dev/null +++ b/lib/views/settings/data/import_scrips_list/show_scrips.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:folio/views/settings/data/import_scrips_list/import_scrips.dart'; +import 'package:folio/views/settings/data/import_scrips_list/parsed_scrips.dart'; + +class ShowScrips extends StatelessWidget { + final ParsedScripsList _scripsList; + + ShowScrips(this._scripsList); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: Theme.of(context).colorScheme.background, + title: Text("Importing Scrips for ${_scripsList.exchange}"), + centerTitle: true, + actions: [ + IconButton( + icon: Icon(Icons.check), + onPressed: () async { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => ImportScrips(_scripsList))); + }, + ) + ], + ), + body: CustomScrollView( + slivers: [ + SliverList.builder( + itemBuilder: (context, index) { + return ParsedScripTile(_scripsList.newScrips[index]); + }, + itemCount: _scripsList.newScrips.length, + ), + ], + ), + bottomNavigationBar: BottomAppBar( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Total: ${_scripsList.newScrips.length}", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/views/settings/data/scrip_tile.dart b/lib/views/settings/data/scrip_tile.dart new file mode 100644 index 0000000..12ab8ac --- /dev/null +++ b/lib/views/settings/data/scrip_tile.dart @@ -0,0 +1,290 @@ +import 'package:flutter/material.dart'; + +import 'package:folio/models/database/scrip.dart'; + +import 'package:folio/helpers/database_actions.dart'; + +import 'import_scrips_list/parsed_scrips.dart'; + +class ScripTile extends StatefulWidget { + final Scrip _scrip; + final Function _refreshParent; + + ScripTile(Scrip scrip, {required void Function() refreshParent}) : this._scrip = scrip, this._refreshParent = refreshParent; + + @override + State createState() => _ScripTileState(); +} + +class _ScripTileState extends State { + bool _showOptions = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Card( + shape: RoundedRectangleBorder( + side: BorderSide(width: 1), + borderRadius: BorderRadius.circular(10), + ), + borderOnForeground: false, + margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), + elevation: 2, + child: Column( + children: [ + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), + elevation: 4, + child: InkWell( + onTap: () { + setState(() { + _showOptions = !_showOptions; + }); + }, + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + widget._scrip.name, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 5.0), + child: Row( + children: [ + Expanded( + flex: 1, + child: Center( + child: Text( + "BSE", + textAlign: TextAlign.center, + ), + ), + ), + Expanded( + flex: 1, + child: Center( + child: Text( + "NSE", + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ), + Row( + children: [ + CodeWidget(code: widget._scrip.bseCode), + SizedBox(width: 10), + CodeWidget(code: widget._scrip.nseCode), + ], + ), + widget._scrip.oldBSECodes.length == 0 && + widget._scrip.oldNSECodes.length == 0 + ? SizedBox() + : Column( + children: [ + Divider( + thickness: 1, + ), + Center(child: Text("OLD CODES")), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 1, + child: Column( + children: [ + Wrap( + spacing: 5, + children: widget._scrip.oldBSECodes + .map((code) => + CodeChip(code: code)) + .toList(), + ), + ], + ), + ), + SizedBox(width: 10), + Expanded( + flex: 1, + child: Column( + children: [ + Wrap( + spacing: 5, + children: widget._scrip.oldNSECodes + .map((code) => + CodeChip(code: code)) + .toList(), + ), + ], + ), + ), + ], + ), + ], + ) + ], + ), + ), + ), + ), + _showOptions + ? Row( + children: [ + Expanded( + child: Center( + child: IconButton( + icon: Icon( + Icons.edit, + color: Colors.blueAccent, + ), + onPressed: editScrip, + ), + ), + ), + Expanded( + child: Center( + child: IconButton( + icon: Icon( + Icons.delete, + color: Colors.redAccent, + ), + onPressed: deleteScrip, + ), + ), + ) + ], + ) + : SizedBox() + ], + ), + ), + ); + } + + editScrip() {} + + deleteScrip() async { + String? result = await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Center(child: Text("WARNING")), + content: Text( + "Are you sure you want to delete ${widget._scrip.name}?", + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.justify, + ), + actions: [ + TextButton( + style: TextButton.styleFrom( + minimumSize: Size(88, 36), + padding: EdgeInsets.symmetric(horizontal: 16), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + ), + onPressed: () { + Navigator.pop(context, "Delete"); + }, + child: Text("Delete"), + ), + SizedBox( + width: 5, + ), + ElevatedButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.background, + minimumSize: Size(88, 36), + padding: EdgeInsets.symmetric(horizontal: 16), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + ), + onPressed: () { + Navigator.pop(context, "Cancel"); + }, + child: Text("Cancel"), + ), + ], + actionsPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + ), + ); + if (result == "Delete") { + try { + await DatabaseActions.deleteScrip(widget._scrip.stockID); + widget._refreshParent(); + } catch (e) { + await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Center(child: Text("Error")), + content: Text( + "Error Occurred: ${e.toString()}", + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.justify, + ), + actions: [ + ElevatedButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.background, + minimumSize: Size(88, 36), + padding: EdgeInsets.symmetric(horizontal: 16), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + ), + onPressed: () { + Navigator.pop(context); + }, + child: Text("Ok"), + ), + ], + actionsPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + ), + ); + } + } + } +} + +class CodeWidget extends StatelessWidget { + const CodeWidget({ + Key? key, + required String? code, + }) : _code = code, + super(key: key); + + final String? _code; + + @override + Widget build(BuildContext context) { + return Expanded( + flex: 1, + child: Center( + child: _code == null + ? Text( + "INACTIVE", + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.w100), + ) + : CodeChip(code: _code!), + ), + ); + } +} \ No newline at end of file diff --git a/lib/views/settings/data/scrips_list.dart b/lib/views/settings/data/scrips_list.dart new file mode 100644 index 0000000..8e6512b --- /dev/null +++ b/lib/views/settings/data/scrips_list.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:folio/helpers/database_actions.dart'; +import 'package:folio/models/database/scrip.dart'; +import 'package:folio/views/settings/data/import_scrips_list/select_scrips_file.dart'; +import 'package:folio/views/settings/data/scrip_tile.dart'; + +class ShowSecuritiesRoute extends StatefulWidget { + @override + State createState() => _ShowSecuritiesRouteState(); +} + +class _ShowSecuritiesRouteState extends State { + late Future?> _scripsListFuture; + + @override + void initState() { + super.initState(); + _scripsListFuture = DatabaseActions.getAllScrips(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: Theme.of(context).colorScheme.background, + title: Text("Securities"), + actions: [ + IconButton( + icon: Icon(Icons.plus_one), + onPressed: () async { + // Navigator.pushReplacement( + // context, + // MaterialPageRoute( + // builder: (context) => ImportScrip),); + }, + ), + IconButton( + icon: Icon(Icons.note_add_outlined), + onPressed: () async { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ImportScripsFileRoute()), + ); + }, + ) + ], + ), + body: CustomScrollView( + slivers: [ + FutureBuilder( + future: _scripsListFuture, + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return SliverFillRemaining( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: LinearProgressIndicator(), + ), + ), + ); + } + if (snapshot.hasError) { + return SliverFillRemaining( + child: Center( + child: Text( + "Error Occurred", + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ); + } + if ((snapshot.data?.length ?? 0) == 0) { + return SliverFillRemaining( + child: Center( + child: Text( + "No Securities Found", + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ); + } + return SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return ScripTile(snapshot.data?[index], refreshParent: refreshList); + }, + childCount: (snapshot.data?.length ?? 0), + ), + ); + }, + ), + ], + ), + ); + } + + void refreshList() { + setState(() { + _scripsListFuture = DatabaseActions.getAllScrips(); + }); + } +} \ No newline at end of file diff --git a/lib/views/settings/drive/drive.dart b/lib/views/settings/drive/drive.dart index 098fbcd..dbca816 100644 --- a/lib/views/settings/drive/drive.dart +++ b/lib/views/settings/drive/drive.dart @@ -284,6 +284,7 @@ class _DriveAreaState extends State { .get(id, downloadOptions: drive.DownloadOptions.fullMedia); final path = await DatabaseActions.getDbPath(); + // TODO: Check if this works final saveFile = File(path); await saveFile.writeAsString(file.toString(), flush: true); }