diff --git a/packages/uni_app/assets/images/current_account.png b/packages/uni_app/assets/images/current_account.png
new file mode 100644
index 000000000..57cd7b88b
Binary files /dev/null and b/packages/uni_app/assets/images/current_account.png differ
diff --git a/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj b/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj
index 9f321be0b..3c1df5698 100644
--- a/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj
@@ -360,10 +360,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -702,6 +706,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = uni_dev;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = 9D8RT43WU8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -709,7 +714,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = pt.up.fe.ni.uni.dev;
+ PRODUCT_BUNDLE_IDENTIFIER = pt.up.fe.ni.uni.dev.g;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
diff --git a/packages/uni_app/ios/Runner/Info.plist b/packages/uni_app/ios/Runner/Info.plist
index e3c79a662..86cd095bc 100644
--- a/packages/uni_app/ios/Runner/Info.plist
+++ b/packages/uni_app/ios/Runner/Info.plist
@@ -55,6 +55,11 @@
LSRequiresIPhoneOS
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
NSCalendarsUsageDescription
Exportar exames e eventos para o calendário
NSPhotoLibraryUsageDescription
diff --git a/packages/uni_app/lib/controller/fetchers/fees_fetcher.dart b/packages/uni_app/lib/controller/fetchers/fees_fetcher.dart
index bd87a5ebc..1a52259b0 100644
--- a/packages/uni_app/lib/controller/fetchers/fees_fetcher.dart
+++ b/packages/uni_app/lib/controller/fetchers/fees_fetcher.dart
@@ -1,6 +1,8 @@
import 'package:http/http.dart';
import 'package:uni/controller/fetchers/session_dependant_fetcher.dart';
import 'package:uni/controller/networking/network_router.dart';
+import 'package:uni/controller/parsers/parser_current_account.dart';
+import 'package:uni/model/entities/current_account.dart';
import 'package:uni/session/flows/base/session.dart';
class FeesFetcher implements SessionDependantFetcher {
@@ -19,4 +21,17 @@ class FeesFetcher implements SessionDependantFetcher {
final query = {'pct_cod': session.username};
return NetworkRouter.getWithCookies(url, query, session);
}
+
+ Future<(List, List)> extractCurrentAccount(
+ Session session,
+ CurrentAccountParser parser,
+ ) async {
+ final response = await getUserFeesResponse(session);
+
+ final unpaid = parser.parseUnpaid(response);
+
+ final accountStatement = parser.parseAccountStatement(response);
+
+ return (unpaid, accountStatement);
+ }
}
diff --git a/packages/uni_app/lib/controller/parsers/parser_current_account.dart b/packages/uni_app/lib/controller/parsers/parser_current_account.dart
new file mode 100644
index 000000000..512201abb
--- /dev/null
+++ b/packages/uni_app/lib/controller/parsers/parser_current_account.dart
@@ -0,0 +1,137 @@
+import 'package:html/dom.dart';
+import 'package:html/parser.dart';
+import 'package:http/http.dart';
+import 'package:uni/model/entities/current_account.dart';
+
+class CurrentAccountParser {
+ static const tabNames = {
+ 'unpaid': ['Despesas não saldadas', 'Unpaid expenses'],
+ //'certificate': ['Certidão', 'Certidão'],
+ //'latePayment': ['Juros de mora Propinas', 'Juros de mora Propinas'],
+ //'tuitionFees': ['Propinas', 'Tuition fees'],
+ //'schoolInsurance': ['Seguro Escolar', 'Seguro Escolar'],
+ 'accountStatement': ['Extrato Geral', 'Account Statement'],
+ };
+
+ String? findTableId(Document document, List names) {
+ final tabs = document.querySelectorAll(
+ '#GPAG_CCORRENTE_GERAL_CONTA_CORRENTE_VIEW ul li a',
+ );
+ for (final tab in tabs) {
+ if (names.contains(tab.text.trim())) {
+ return tab.attributes['href'];
+ }
+ }
+ return null;
+ }
+
+ List parseUnpaid(Response response) {
+ final List data = [];
+ final document = parse(response.body);
+
+ final tableId = findTableId(document, tabNames['unpaid']!);
+
+ if (tableId != null) {
+ final tab = document.querySelector(tableId);
+ final rows = tab?.querySelectorAll('tr').skip(1) ?? [];
+
+ for (final row in rows) {
+ final cells = row.querySelectorAll('td');
+ final description = cells[2].text.trim();
+
+ final date = DateTime.parse(cells[3].text.trim());
+
+ final deadline = cells[4].text.trim().isEmpty
+ ? null
+ : DateTime.parse(cells[4].text.trim());
+ final value = parseAmount(cells[5].text.trim()) ?? 0;
+ final amountDue = parseAmount(cells[7].text.trim()) ?? 0;
+
+ final anchor = cells[8].querySelector('a');
+ final relativeLink = anchor?.attributes['href'];
+
+ String? paymentLink;
+ if (relativeLink != null) {
+ paymentLink = 'https://sigarra.up.pt/feup/pt/$relativeLink';
+ }
+
+ final interest = cells[9].text.trim();
+ double? interestOnLatePayment;
+
+ if (interest.isNotEmpty) {
+ final parts = interest.split('+');
+ interestOnLatePayment = parts
+ .map((p) => parseAmount(p) ?? 0.0)
+ .reduce((a, b) => a + b);
+ }
+
+ data.add(
+ Unpaid(
+ description: description,
+ date: date,
+ deadline: deadline,
+ value: value,
+ amountDue: amountDue,
+ interestOnLatePayment: interestOnLatePayment,
+ paymentLink: paymentLink,
+ ),
+ );
+ }
+ }
+ return data;
+ }
+
+ List parseAccountStatement(Response response) {
+ final document = parse(response.body);
+ final List data = [];
+
+ final tableId = findTableId(document, tabNames['accountStatement']!);
+
+ if (tableId != null) {
+ final tab = document.querySelector(tableId);
+ final rows = tab?.querySelectorAll('tbody tr') ?? [];
+
+ for (final row in rows) {
+ final cells = row.querySelectorAll('td');
+ final description = cells[0].text.trim();
+
+ final date = DateTime.parse(cells[1].text.trim());
+ final credit = parseAmount(cells[3].text.trim());
+
+ if (credit != null) {
+ data.add(
+ AccountStatement(
+ description: description,
+ date: date,
+ credit: credit,
+ ),
+ );
+ }
+ }
+ }
+
+ return data;
+ }
+
+ String parseStatus(Element cell) {
+ final img = cell.querySelector('img');
+ final alt = img?.attributes['alt'] ?? '';
+
+ return switch (alt) {
+ 'Pago' => 'Paid',
+ 'Não pago mas prazo ainda não foi excedido' => 'unpaid',
+ 'Não pago mas prazo foi excedido' => 'overdue',
+ _ => 'unknown',
+ };
+ }
+
+ double? parseAmount(String text) {
+ final cleaned = text
+ .replaceAll('€', '')
+ .replaceAll('\u00a0', '')
+ .replaceAll(' ', '')
+ .replaceAll(',', '');
+
+ return double.tryParse(cleaned);
+ }
+}
diff --git a/packages/uni_app/lib/generated/intl/messages_en.dart b/packages/uni_app/lib/generated/intl/messages_en.dart
index ec226d08b..b3b493f06 100644
--- a/packages/uni_app/lib/generated/intl/messages_en.dart
+++ b/packages/uni_app/lib/generated/intl/messages_en.dart
@@ -28,7 +28,7 @@ class MessageLookup extends MessageLookupByLibrary {
"${Intl.plural(time, zero: 'Refreshed ${time} minutes ago', one: 'Refreshed ${time} minute ago', other: 'Refreshed ${time} minutes ago')}";
static m3(title) =>
- "${Intl.select(title, {'horario': 'Schedule', 'exames': 'Exams', 'area': 'Personal Area', 'cadeiras': 'Course Units', 'autocarros': 'Buses', 'locais': 'Places', 'restaurantes': 'Restaurants', 'calendario': 'Calendar', 'biblioteca': 'Library', 'percurso_academico': 'Academic Path', 'mapa': 'Map', 'faculdade': 'Faculty', 'other': 'Other'})}";
+ "${Intl.select(title, {'horario': 'Schedule', 'exames': 'Exams', 'area': 'Personal Area', 'cadeiras': 'Course Units', 'autocarros': 'Buses', 'locais': 'Places', 'restaurantes': 'Restaurants', 'calendario': 'Calendar', 'biblioteca': 'Library', 'percurso_academico': 'Academic Path', 'mapa': 'Map', 'faculdade': 'Faculty', 'conta_corrente': 'Current Account', 'other': 'Other'})}";
static m4(period) =>
"${Intl.select(period, {'lunch': 'Lunch', 'dinner': 'Dinner', 'other': 'Other'})}";
@@ -71,6 +71,9 @@ class MessageLookup extends MessageLookupByLibrary {
),
"average": MessageLookupByLibrary.simpleMessage("Average"),
"balance": MessageLookupByLibrary.simpleMessage("Balance"),
+ "balance_description": MessageLookupByLibrary.simpleMessage(
+ "Your total outstanding balance",
+ ),
"banner_info": MessageLookupByLibrary.simpleMessage(
"We collect anonymous usage data to help improve your experience. You can opt out anytime in the settings.",
),
@@ -146,6 +149,10 @@ class MessageLookup extends MessageLookupByLibrary {
"course_class": MessageLookupByLibrary.simpleMessage("Classes"),
"course_info": MessageLookupByLibrary.simpleMessage("Info"),
"courses": MessageLookupByLibrary.simpleMessage("Courses"),
+ "current_account": MessageLookupByLibrary.simpleMessage("Current Account"),
+ "current_account_description": MessageLookupByLibrary.simpleMessage(
+ "Track your fees, due dates and payment history.",
+ ),
"current_state": MessageLookupByLibrary.simpleMessage("Current state: "),
"current_year": MessageLookupByLibrary.simpleMessage(
"Current academic year: ",
@@ -173,6 +180,7 @@ class MessageLookup extends MessageLookupByLibrary {
"drag_and_drop": MessageLookupByLibrary.simpleMessage(
"Drag and drop elements",
),
+ "due_in": MessageLookupByLibrary.simpleMessage("Due in"),
"ects": MessageLookupByLibrary.simpleMessage("ECTS performed: "),
"edit_homepage": MessageLookupByLibrary.simpleMessage("Edit"),
"edit_off": MessageLookupByLibrary.simpleMessage("Edit"),
@@ -199,6 +207,9 @@ class MessageLookup extends MessageLookupByLibrary {
"failed_upload": MessageLookupByLibrary.simpleMessage("Failed to upload"),
"favorite_filter": MessageLookupByLibrary.simpleMessage("Favorites"),
"fee_date": MessageLookupByLibrary.simpleMessage("Deadline"),
+ "fee_date_description": MessageLookupByLibrary.simpleMessage(
+ "Due date for your next payment",
+ ),
"fee_notification": MessageLookupByLibrary.simpleMessage("Fee deadline"),
"feedback_description": MessageLookupByLibrary.simpleMessage(
"Report an issue or suggest an improvement",
@@ -211,6 +222,7 @@ class MessageLookup extends MessageLookupByLibrary {
"floors": MessageLookupByLibrary.simpleMessage("Floors"),
"forgot_password": MessageLookupByLibrary.simpleMessage("Forgot password?"),
"frequency": MessageLookupByLibrary.simpleMessage("Eligibility for exams"),
+ "general_history": MessageLookupByLibrary.simpleMessage("General History"),
"generate_reference": MessageLookupByLibrary.simpleMessage(
"Generate reference",
),
@@ -226,6 +238,9 @@ class MessageLookup extends MessageLookupByLibrary {
"increment": MessageLookupByLibrary.simpleMessage("Increment 1,00€"),
"instructor": MessageLookupByLibrary.simpleMessage("Instructor"),
"instructors": MessageLookupByLibrary.simpleMessage("Instructors"),
+ "interest_on_late_payments": MessageLookupByLibrary.simpleMessage(
+ "Interest on late payments",
+ ),
"internet_status_exception": MessageLookupByLibrary.simpleMessage(
"Check your internet connection",
),
@@ -305,6 +320,9 @@ class MessageLookup extends MessageLookupByLibrary {
"no_courses_description": MessageLookupByLibrary.simpleMessage(
"Try to refresh the page",
),
+ "no_current_account_info": MessageLookupByLibrary.simpleMessage(
+ "Try refreshing the page or check back later.",
+ ),
"no_data": MessageLookupByLibrary.simpleMessage(
"There is no data to show at this time",
),
@@ -326,6 +344,12 @@ class MessageLookup extends MessageLookupByLibrary {
"no_files_label": MessageLookupByLibrary.simpleMessage(
"You have nothing to see!",
),
+ "no_history_label": MessageLookupByLibrary.simpleMessage(
+ "Nothing here yet",
+ ),
+ "no_history_sublabel": MessageLookupByLibrary.simpleMessage(
+ "Your payment history will appear here.",
+ ),
"no_info": MessageLookupByLibrary.simpleMessage(
"There is no information to display",
),
@@ -349,12 +373,17 @@ class MessageLookup extends MessageLookupByLibrary {
),
"no_name_course": MessageLookupByLibrary.simpleMessage("Unnamed course"),
"no_news": MessageLookupByLibrary.simpleMessage("No news to display"),
+ "no_pending_label": MessageLookupByLibrary.simpleMessage("All caught up!"),
+ "no_pending_sublabel": MessageLookupByLibrary.simpleMessage(
+ "You have no pending payments.",
+ ),
"no_places_info": MessageLookupByLibrary.simpleMessage(
"There is no information available about places",
),
"no_print_info": MessageLookupByLibrary.simpleMessage(
"No print balance information",
),
+ "no_records": MessageLookupByLibrary.simpleMessage("No records"),
"no_references": MessageLookupByLibrary.simpleMessage(
"There are no references to pay",
),
@@ -374,6 +403,12 @@ class MessageLookup extends MessageLookupByLibrary {
"no_trips": MessageLookupByLibrary.simpleMessage(
"No trips found at the moment",
),
+ "no_tuition_fees_label": MessageLookupByLibrary.simpleMessage(
+ "No tuition fees found",
+ ),
+ "no_tuition_fees_sublabel": MessageLookupByLibrary.simpleMessage(
+ "Your tuition fee records will appear here.",
+ ),
"notifications": MessageLookupByLibrary.simpleMessage("Notifications"),
"now": MessageLookupByLibrary.simpleMessage("Now"),
"occurrence_type": MessageLookupByLibrary.simpleMessage(
@@ -384,6 +419,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Error opening the file",
),
"other_links": MessageLookupByLibrary.simpleMessage("Other links"),
+ "overview": MessageLookupByLibrary.simpleMessage("Overview"),
"pass_change_request": MessageLookupByLibrary.simpleMessage(
"For security reasons, passwords must be changed periodically.",
),
@@ -397,6 +433,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pendent_references": MessageLookupByLibrary.simpleMessage(
"Pending references",
),
+ "pending": MessageLookupByLibrary.simpleMessage("Pending"),
"permission_denied": MessageLookupByLibrary.simpleMessage(
"Permission denied",
),
@@ -406,6 +443,9 @@ class MessageLookup extends MessageLookupByLibrary {
"press_again": MessageLookupByLibrary.simpleMessage("Press again to exit"),
"print": MessageLookupByLibrary.simpleMessage("Print"),
"print_balance": MessageLookupByLibrary.simpleMessage("Print balance"),
+ "print_balance_description": MessageLookupByLibrary.simpleMessage(
+ "Funds for university printing services",
+ ),
"prints": MessageLookupByLibrary.simpleMessage("Prints"),
"problem_id": MessageLookupByLibrary.simpleMessage(
"Brief identification of the problem",
@@ -468,10 +508,12 @@ class MessageLookup extends MessageLookupByLibrary {
"theme": MessageLookupByLibrary.simpleMessage("Theme"),
"title": MessageLookupByLibrary.simpleMessage("Title"),
"tomorrows_meals": MessageLookupByLibrary.simpleMessage("Tomorrow\'s Menu"),
+ "transactions": MessageLookupByLibrary.simpleMessage("Transactions"),
"try_again": MessageLookupByLibrary.simpleMessage("Try again"),
"try_different_login": MessageLookupByLibrary.simpleMessage(
"Having trouble signing in?",
),
+ "tuition_fees": MessageLookupByLibrary.simpleMessage("Tuition Fees"),
"uc_info": MessageLookupByLibrary.simpleMessage("Open UC page"),
"ucs": MessageLookupByLibrary.simpleMessage("UCS"),
"unable_to_load_data": MessageLookupByLibrary.simpleMessage(
@@ -479,6 +521,7 @@ class MessageLookup extends MessageLookupByLibrary {
),
"unavailable": MessageLookupByLibrary.simpleMessage("Unavailable"),
"until": MessageLookupByLibrary.simpleMessage("Until"),
+ "upcoming_due": MessageLookupByLibrary.simpleMessage("Upcoming Due"),
"valid_email": MessageLookupByLibrary.simpleMessage(
"Please enter a valid email",
),
diff --git a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart
index db7600f51..bd6fc72cb 100644
--- a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart
+++ b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart
@@ -28,7 +28,7 @@ class MessageLookup extends MessageLookupByLibrary {
"${Intl.plural(time, zero: 'Atualizado há ${time} minutos', one: 'Atualizado há ${time} minuto', other: 'Atualizado há ${time} minutos')}";
static m3(title) =>
- "${Intl.select(title, {'horario': 'Horário', 'exames': 'Exames', 'area': 'Área Pessoal', 'cadeiras': 'Cadeiras', 'autocarros': 'Autocarros', 'locais': 'Locais', 'restaurantes': 'Restaurantes', 'calendario': 'Calendário', 'biblioteca': 'Biblioteca', 'percurso_academico': 'Percurso Académico', 'mapa': 'Mapa', 'faculdade': 'Faculdade', 'other': 'Outros'})}";
+ "${Intl.select(title, {'horario': 'Horário', 'exames': 'Exames', 'area': 'Área Pessoal', 'cadeiras': 'Cadeiras', 'autocarros': 'Autocarros', 'locais': 'Locais', 'restaurantes': 'Restaurantes', 'calendario': 'Calendário', 'biblioteca': 'Biblioteca', 'percurso_academico': 'Percurso Académico', 'mapa': 'Mapa', 'faculdade': 'Faculdade', 'conta_corrente': 'Conta Corrente', 'other': 'Outros'})}";
static m4(period) =>
"${Intl.select(period, {'lunch': 'Almoço', 'dinner': 'Jantar', 'other': 'Other'})}";
@@ -75,6 +75,9 @@ class MessageLookup extends MessageLookupByLibrary {
),
"average": MessageLookupByLibrary.simpleMessage("Média"),
"balance": MessageLookupByLibrary.simpleMessage("Saldo"),
+ "balance_description": MessageLookupByLibrary.simpleMessage(
+ "O teu saldo total em dívida",
+ ),
"banner_info": MessageLookupByLibrary.simpleMessage(
"Recolhemos dados anónimos de utilização para ajudar a melhorar a sua experiência. Pode desativar esta opção a qualquer momento nas definições",
),
@@ -154,6 +157,10 @@ class MessageLookup extends MessageLookupByLibrary {
"course_class": MessageLookupByLibrary.simpleMessage("Turmas"),
"course_info": MessageLookupByLibrary.simpleMessage("Ficha"),
"courses": MessageLookupByLibrary.simpleMessage("Cursos"),
+ "current_account": MessageLookupByLibrary.simpleMessage("Conta Corrente"),
+ "current_account_description": MessageLookupByLibrary.simpleMessage(
+ "Acompanha as tuas propinas, prazos e histórico de pagamentos.",
+ ),
"current_state": MessageLookupByLibrary.simpleMessage("Estado atual: "),
"current_year": MessageLookupByLibrary.simpleMessage(
"Ano curricular atual: ",
@@ -179,6 +186,7 @@ class MessageLookup extends MessageLookupByLibrary {
"drag_and_drop": MessageLookupByLibrary.simpleMessage(
"Arrasta e solta os elementos",
),
+ "due_in": MessageLookupByLibrary.simpleMessage("Vence em"),
"ects": MessageLookupByLibrary.simpleMessage("ECTS realizados: "),
"edit_homepage": MessageLookupByLibrary.simpleMessage("Editar"),
"edit_off": MessageLookupByLibrary.simpleMessage("Editar"),
@@ -207,6 +215,9 @@ class MessageLookup extends MessageLookupByLibrary {
),
"favorite_filter": MessageLookupByLibrary.simpleMessage("Favoritos"),
"fee_date": MessageLookupByLibrary.simpleMessage("Data limite"),
+ "fee_date_description": MessageLookupByLibrary.simpleMessage(
+ "Data limite para o próximo pagamento",
+ ),
"fee_notification": MessageLookupByLibrary.simpleMessage(
"Data limite de propina",
),
@@ -223,6 +234,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Esqueceu a palavra-passe?",
),
"frequency": MessageLookupByLibrary.simpleMessage("Obtenção de Frequência"),
+ "general_history": MessageLookupByLibrary.simpleMessage("Histórico Geral"),
"generate_reference": MessageLookupByLibrary.simpleMessage(
"Gerar referência",
),
@@ -238,6 +250,9 @@ class MessageLookup extends MessageLookupByLibrary {
"increment": MessageLookupByLibrary.simpleMessage("Incrementar 1,00€"),
"instructor": MessageLookupByLibrary.simpleMessage("Docente"),
"instructors": MessageLookupByLibrary.simpleMessage("Docentes"),
+ "interest_on_late_payments": MessageLookupByLibrary.simpleMessage(
+ "Juros de mora",
+ ),
"internet_status_exception": MessageLookupByLibrary.simpleMessage(
"Verifique sua conexão com a internet",
),
@@ -321,6 +336,9 @@ class MessageLookup extends MessageLookupByLibrary {
"no_courses_description": MessageLookupByLibrary.simpleMessage(
"Tenta refrescar a página",
),
+ "no_current_account_info": MessageLookupByLibrary.simpleMessage(
+ "Tenta atualizar a página ou volta mais tarde.",
+ ),
"no_data": MessageLookupByLibrary.simpleMessage(
"Não há dados a mostrar neste momento",
),
@@ -346,6 +364,10 @@ class MessageLookup extends MessageLookupByLibrary {
"no_files_label": MessageLookupByLibrary.simpleMessage(
"Não tens nada para ver!",
),
+ "no_history_label": MessageLookupByLibrary.simpleMessage("Sem movimentos"),
+ "no_history_sublabel": MessageLookupByLibrary.simpleMessage(
+ "O teu histórico de pagamentos aparecerá aqui.",
+ ),
"no_info": MessageLookupByLibrary.simpleMessage(
"Não existem informações para apresentar",
),
@@ -371,12 +393,17 @@ class MessageLookup extends MessageLookupByLibrary {
"no_news": MessageLookupByLibrary.simpleMessage(
"Não há notícias para apresentar",
),
+ "no_pending_label": MessageLookupByLibrary.simpleMessage("Tudo em dia!"),
+ "no_pending_sublabel": MessageLookupByLibrary.simpleMessage(
+ "Não tens pagamentos pendentes.",
+ ),
"no_places_info": MessageLookupByLibrary.simpleMessage(
"Não há informação disponível sobre locais",
),
"no_print_info": MessageLookupByLibrary.simpleMessage(
"Sem informação de saldo",
),
+ "no_records": MessageLookupByLibrary.simpleMessage("Sem registos"),
"no_references": MessageLookupByLibrary.simpleMessage(
"Não existem referências a pagar",
),
@@ -396,6 +423,12 @@ class MessageLookup extends MessageLookupByLibrary {
"no_trips": MessageLookupByLibrary.simpleMessage(
"Não há viagens planeadas de momento",
),
+ "no_tuition_fees_label": MessageLookupByLibrary.simpleMessage(
+ "Sem propinas encontradas",
+ ),
+ "no_tuition_fees_sublabel": MessageLookupByLibrary.simpleMessage(
+ "Os teus registos de propinas aparecerão aqui.",
+ ),
"notifications": MessageLookupByLibrary.simpleMessage("Notificações"),
"now": MessageLookupByLibrary.simpleMessage("Agora"),
"occurrence_type": MessageLookupByLibrary.simpleMessage(
@@ -406,6 +439,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Erro ao abrir o ficheiro",
),
"other_links": MessageLookupByLibrary.simpleMessage("Outros links"),
+ "overview": MessageLookupByLibrary.simpleMessage("Visão Geral"),
"pass_change_request": MessageLookupByLibrary.simpleMessage(
"Por razões de segurança, as palavras-passe têm de ser alteradas periodicamente.",
),
@@ -419,6 +453,7 @@ class MessageLookup extends MessageLookupByLibrary {
"pendent_references": MessageLookupByLibrary.simpleMessage(
"Referências pendentes",
),
+ "pending": MessageLookupByLibrary.simpleMessage("Pendentes"),
"permission_denied": MessageLookupByLibrary.simpleMessage("Sem permissão"),
"personal_assistance": MessageLookupByLibrary.simpleMessage(
"Atendimento presencial",
@@ -428,6 +463,9 @@ class MessageLookup extends MessageLookupByLibrary {
),
"print": MessageLookupByLibrary.simpleMessage("Impressão"),
"print_balance": MessageLookupByLibrary.simpleMessage("Saldo impressões"),
+ "print_balance_description": MessageLookupByLibrary.simpleMessage(
+ "Saldo para serviços de impressão da UP",
+ ),
"prints": MessageLookupByLibrary.simpleMessage("Impressões"),
"problem_id": MessageLookupByLibrary.simpleMessage(
"Breve identificação do problema",
@@ -496,10 +534,12 @@ class MessageLookup extends MessageLookupByLibrary {
"theme": MessageLookupByLibrary.simpleMessage("Tema"),
"title": MessageLookupByLibrary.simpleMessage("Título"),
"tomorrows_meals": MessageLookupByLibrary.simpleMessage("Menu de Amanhã"),
+ "transactions": MessageLookupByLibrary.simpleMessage("Transações"),
"try_again": MessageLookupByLibrary.simpleMessage("Tentar de novo"),
"try_different_login": MessageLookupByLibrary.simpleMessage(
"Problemas ao iniciar sessão?",
),
+ "tuition_fees": MessageLookupByLibrary.simpleMessage("Propinas"),
"uc_info": MessageLookupByLibrary.simpleMessage("Abrir página da UC"),
"ucs": MessageLookupByLibrary.simpleMessage("UCS"),
"unable_to_load_data": MessageLookupByLibrary.simpleMessage(
@@ -507,6 +547,7 @@ class MessageLookup extends MessageLookupByLibrary {
),
"unavailable": MessageLookupByLibrary.simpleMessage("Indisponível"),
"until": MessageLookupByLibrary.simpleMessage("Até"),
+ "upcoming_due": MessageLookupByLibrary.simpleMessage("Próximo Vencimento"),
"valid_email": MessageLookupByLibrary.simpleMessage(
"Por favor insere um email válido",
),
diff --git a/packages/uni_app/lib/generated/l10n.dart b/packages/uni_app/lib/generated/l10n.dart
index 0081a27f5..0c326f4bf 100644
--- a/packages/uni_app/lib/generated/l10n.dart
+++ b/packages/uni_app/lib/generated/l10n.dart
@@ -792,7 +792,7 @@ class S {
);
}
- /// `{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} percurso_academico{Academic Path} mapa{Map} faculdade{Faculty} other{Other}}`
+ /// `{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} percurso_academico{Academic Path} mapa{Map} faculdade{Faculty} conta_corrente{Current Account} other{Other}}`
String nav_title(Object title) {
return Intl.select(
title,
@@ -809,6 +809,7 @@ class S {
'percurso_academico': 'Academic Path',
'mapa': 'Map',
'faculdade': 'Faculty',
+ 'conta_corrente': 'Current Account',
'other': 'Other',
},
name: 'nav_title',
@@ -2030,6 +2031,196 @@ class S {
);
}
+ /// `Current Account`
+ String get current_account {
+ return Intl.message(
+ 'Current Account',
+ name: 'current_account',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Track your fees, due dates and payment history.`
+ String get current_account_description {
+ return Intl.message(
+ 'Track your fees, due dates and payment history.',
+ name: 'current_account_description',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Overview`
+ String get overview {
+ return Intl.message('Overview', name: 'overview', desc: '', args: []);
+ }
+
+ /// `Upcoming Due`
+ String get upcoming_due {
+ return Intl.message(
+ 'Upcoming Due',
+ name: 'upcoming_due',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Transactions`
+ String get transactions {
+ return Intl.message(
+ 'Transactions',
+ name: 'transactions',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Pending`
+ String get pending {
+ return Intl.message('Pending', name: 'pending', desc: '', args: []);
+ }
+
+ /// `Tuition Fees`
+ String get tuition_fees {
+ return Intl.message(
+ 'Tuition Fees',
+ name: 'tuition_fees',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `General History`
+ String get general_history {
+ return Intl.message(
+ 'General History',
+ name: 'general_history',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Your total outstanding balance`
+ String get balance_description {
+ return Intl.message(
+ 'Your total outstanding balance',
+ name: 'balance_description',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Due date for your next payment`
+ String get fee_date_description {
+ return Intl.message(
+ 'Due date for your next payment',
+ name: 'fee_date_description',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Funds for university printing services`
+ String get print_balance_description {
+ return Intl.message(
+ 'Funds for university printing services',
+ name: 'print_balance_description',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Interest on late payments`
+ String get interest_on_late_payments {
+ return Intl.message(
+ 'Interest on late payments',
+ name: 'interest_on_late_payments',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Due in`
+ String get due_in {
+ return Intl.message('Due in', name: 'due_in', desc: '', args: []);
+ }
+
+ /// `No records`
+ String get no_records {
+ return Intl.message('No records', name: 'no_records', desc: '', args: []);
+ }
+
+ /// `Try refreshing the page or check back later.`
+ String get no_current_account_info {
+ return Intl.message(
+ 'Try refreshing the page or check back later.',
+ name: 'no_current_account_info',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `All caught up!`
+ String get no_pending_label {
+ return Intl.message(
+ 'All caught up!',
+ name: 'no_pending_label',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `You have no pending payments.`
+ String get no_pending_sublabel {
+ return Intl.message(
+ 'You have no pending payments.',
+ name: 'no_pending_sublabel',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Nothing here yet`
+ String get no_history_label {
+ return Intl.message(
+ 'Nothing here yet',
+ name: 'no_history_label',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Your payment history will appear here.`
+ String get no_history_sublabel {
+ return Intl.message(
+ 'Your payment history will appear here.',
+ name: 'no_history_sublabel',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `No tuition fees found`
+ String get no_tuition_fees_label {
+ return Intl.message(
+ 'No tuition fees found',
+ name: 'no_tuition_fees_label',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Your tuition fee records will appear here.`
+ String get no_tuition_fees_sublabel {
+ return Intl.message(
+ 'Your tuition fee records will appear here.',
+ name: 'no_tuition_fees_sublabel',
+ desc: '',
+ args: [],
+ );
+ }
+
/// `Edit`
String get edit_homepage {
return Intl.message('Edit', name: 'edit_homepage', desc: '', args: []);
diff --git a/packages/uni_app/lib/l10n/intl_en.arb b/packages/uni_app/lib/l10n/intl_en.arb
index 41597c5d2..ed78f60a2 100644
--- a/packages/uni_app/lib/l10n/intl_en.arb
+++ b/packages/uni_app/lib/l10n/intl_en.arb
@@ -192,7 +192,7 @@
"@min_value_reference": {},
"multimedia_center": "Multimedia center",
"@multimedia_center": {},
- "nav_title": "{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} percurso_academico{Academic Path} mapa{Map} faculdade{Faculty} other{Other}}",
+ "nav_title": "{title, select, horario{Schedule} exames{Exams} area{Personal Area} cadeiras{Course Units} autocarros{Buses} locais{Places} restaurantes{Restaurants} calendario{Calendar} biblioteca{Library} percurso_academico{Academic Path} mapa{Map} faculdade{Faculty} conta_corrente{Current Account} other{Other}}",
"@nav_title": {},
"news": "News",
"@news": {},
@@ -484,6 +484,48 @@
"@no_restaurants_available": {},
"no_restaurants_available_sublabel": "Bring your lunchbox from home.",
"@no_restaurants_available_sublabel": {},
+ "current_account": "Current Account",
+ "@current_account": {},
+ "current_account_description": "Track your fees, due dates and payment history.",
+ "@current_account_description": {},
+ "overview": "Overview",
+ "@overview": {},
+ "upcoming_due": "Upcoming Due",
+ "@upcoming_due": {},
+ "transactions": "Transactions",
+ "@transactions": {},
+ "pending": "Pending",
+ "@pending": {},
+ "tuition_fees": "Tuition Fees",
+ "@tuition_fees": {},
+ "general_history": "General History",
+ "@general_history": {},
+ "balance_description": "Your total outstanding balance",
+ "@balance_description": {},
+ "fee_date_description": "Due date for your next payment",
+ "@fee_date_description": {},
+ "print_balance_description": "Funds for university printing services",
+ "@print_balance_description": {},
+ "interest_on_late_payments": "Interest on late payments",
+ "@interest_on_late_payments": {},
+ "due_in": "Due in",
+ "@due_in": {},
+ "no_records": "No records",
+ "@no_records": {},
+ "no_current_account_info": "Try refreshing the page or check back later.",
+ "@no_current_account_info": {},
+ "no_pending_label": "All caught up!",
+ "@no_pending_label": {},
+ "no_pending_sublabel": "You have no pending payments.",
+ "@no_pending_sublabel": {},
+ "no_history_label": "Nothing here yet",
+ "@no_history_label": {},
+ "no_history_sublabel": "Your payment history will appear here.",
+ "@no_history_sublabel": {},
+ "no_tuition_fees_label": "No tuition fees found",
+ "@no_tuition_fees_label": {},
+ "no_tuition_fees_sublabel": "Your tuition fee records will appear here.",
+ "@no_tuition_fees_sublabel": {},
"edit_homepage": "Edit",
"@edit_homepage": {}
}
\ No newline at end of file
diff --git a/packages/uni_app/lib/l10n/intl_pt_PT.arb b/packages/uni_app/lib/l10n/intl_pt_PT.arb
index 40825cf64..3ce896d82 100644
--- a/packages/uni_app/lib/l10n/intl_pt_PT.arb
+++ b/packages/uni_app/lib/l10n/intl_pt_PT.arb
@@ -200,7 +200,7 @@
"@min_value_reference": {},
"multimedia_center": "Centro de multimédia",
"@multimedia_center": {},
- "nav_title": "{title, select, horario{Horário} exames{Exames} area{Área Pessoal} cadeiras{Cadeiras} autocarros{Autocarros} locais{Locais} restaurantes{Restaurantes} calendario{Calendário} biblioteca{Biblioteca} percurso_academico{Percurso Académico} mapa{Mapa} faculdade{Faculdade} other{Outros}}",
+ "nav_title": "{title, select, horario{Horário} exames{Exames} area{Área Pessoal} cadeiras{Cadeiras} autocarros{Autocarros} locais{Locais} restaurantes{Restaurantes} calendario{Calendário} biblioteca{Biblioteca} percurso_academico{Percurso Académico} mapa{Mapa} faculdade{Faculdade} conta_corrente{Conta Corrente} other{Outros}}",
"@nav_title": {},
"news": "Notícias",
"@news": {},
@@ -484,6 +484,48 @@
"@no_restaurants_available": {},
"no_restaurants_available_sublabel": "Traz a tua marmita de casa.",
"@no_restaurants_available_sublabel": {},
+ "current_account": "Conta Corrente",
+ "@current_account": {},
+ "current_account_description": "Acompanha as tuas propinas, prazos e histórico de pagamentos.",
+ "@current_account_description": {},
+ "overview": "Visão Geral",
+ "@overview": {},
+ "upcoming_due": "Próximo Vencimento",
+ "@upcoming_due": {},
+ "transactions": "Transações",
+ "@transactions": {},
+ "pending": "Pendentes",
+ "@pending": {},
+ "tuition_fees": "Propinas",
+ "@tuition_fees": {},
+ "general_history": "Histórico Geral",
+ "@general_history": {},
+ "balance_description": "O teu saldo total em dívida",
+ "@balance_description": {},
+ "fee_date_description": "Data limite para o próximo pagamento",
+ "@fee_date_description": {},
+ "print_balance_description": "Saldo para serviços de impressão da UP",
+ "@print_balance_description": {},
+ "interest_on_late_payments": "Juros de mora",
+ "@interest_on_late_payments": {},
+ "due_in": "Vence em",
+ "@due_in": {},
+ "no_records": "Sem registos",
+ "@no_records": {},
+ "no_current_account_info": "Tenta atualizar a página ou volta mais tarde.",
+ "@no_current_account_info": {},
+ "no_pending_label": "Tudo em dia!",
+ "@no_pending_label": {},
+ "no_pending_sublabel": "Não tens pagamentos pendentes.",
+ "@no_pending_sublabel": {},
+ "no_history_label": "Sem movimentos",
+ "@no_history_label": {},
+ "no_history_sublabel": "O teu histórico de pagamentos aparecerá aqui.",
+ "@no_history_sublabel": {},
+ "no_tuition_fees_label": "Sem propinas encontradas",
+ "@no_tuition_fees_label": {},
+ "no_tuition_fees_sublabel": "Os teus registos de propinas aparecerão aqui.",
+ "@no_tuition_fees_sublabel": {},
"edit_homepage": "Editar",
"@edit_homepage": {}
}
\ No newline at end of file
diff --git a/packages/uni_app/lib/main.dart b/packages/uni_app/lib/main.dart
index fbefafce8..a301c1b43 100644
--- a/packages/uni_app/lib/main.dart
+++ b/packages/uni_app/lib/main.dart
@@ -27,6 +27,7 @@ import 'package:uni/view/academic_path/academic_path.dart';
import 'package:uni/view/bug_report/bug_report.dart';
import 'package:uni/view/calendar/calendar.dart';
import 'package:uni/view/course_unit_info/course_unit_info.dart';
+import 'package:uni/view/current_account/current_account.dart';
import 'package:uni/view/faculty/faculty.dart';
import 'package:uni/view/home/edit_home.dart';
import 'package:uni/view/home/home.dart';
@@ -250,6 +251,11 @@ class ApplicationState extends ConsumerState {
page: CourseUnitDetailPageView(courseUnit!),
settings: settings,
),
+ '/${NavigationItem.navCurrentAccount.route}': () =>
+ PageTransition.makePageTransition(
+ page: const CurrentAccountPageView(),
+ settings: settings,
+ ),
};
final builder = transitionFunctions[settings.name];
diff --git a/packages/uni_app/lib/model/entities/current_account.dart b/packages/uni_app/lib/model/entities/current_account.dart
new file mode 100644
index 000000000..1f682c09d
--- /dev/null
+++ b/packages/uni_app/lib/model/entities/current_account.dart
@@ -0,0 +1,41 @@
+class Unpaid {
+ Unpaid({
+ required this.description,
+ required this.date,
+ this.deadline,
+ required this.value,
+ required this.amountDue,
+ this.interestOnLatePayment,
+ this.paymentLink,
+ });
+
+ final String description;
+ final DateTime date;
+ final DateTime? deadline;
+ final double value;
+ final double amountDue;
+ final double? interestOnLatePayment;
+ final String? paymentLink;
+
+ @override
+ String toString() {
+ return 'Unpaid(description: $description, date: $date, deadline: $deadline, value: $value, amountDue: $amountDue, interestOnLatePayment: $interestOnLatePayment)';
+ }
+}
+
+class AccountStatement {
+ AccountStatement({
+ required this.description,
+ required this.date,
+ required this.credit,
+ });
+
+ final String description;
+ final DateTime date;
+ final double credit;
+
+ @override
+ String toString() {
+ return 'AccountStatement(description: $description, date: $date, credit:$credit)';
+ }
+}
diff --git a/packages/uni_app/lib/model/providers/riverpod/current_account_provider.dart b/packages/uni_app/lib/model/providers/riverpod/current_account_provider.dart
new file mode 100644
index 000000000..0cb828240
--- /dev/null
+++ b/packages/uni_app/lib/model/providers/riverpod/current_account_provider.dart
@@ -0,0 +1,12 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:uni/controller/fetchers/fees_fetcher.dart';
+import 'package:uni/controller/parsers/parser_current_account.dart';
+import 'package:uni/model/providers/riverpod/session_provider.dart';
+
+final currentAccountProvider = FutureProvider.autoDispose((ref) async {
+ final session = await ref.watch(sessionProvider.future);
+ final fetcher = FeesFetcher();
+ final parser = CurrentAccountParser();
+ final result = await fetcher.extractCurrentAccount(session!, parser);
+ return result;
+});
diff --git a/packages/uni_app/lib/utils/navigation_items.dart b/packages/uni_app/lib/utils/navigation_items.dart
index e70a8dd2b..48dd8a953 100644
--- a/packages/uni_app/lib/utils/navigation_items.dart
+++ b/packages/uni_app/lib/utils/navigation_items.dart
@@ -16,7 +16,8 @@ enum NavigationItem {
navLogin('login'),
navBugreport('bug_report'),
navSplash('splash'),
- navAboutus('sobre_nos');
+ navAboutus('sobre_nos'),
+ navCurrentAccount('conta_corrente');
const NavigationItem(this.route, {this.faculties});
diff --git a/packages/uni_app/lib/view/current_account/current_account.dart b/packages/uni_app/lib/view/current_account/current_account.dart
new file mode 100644
index 000000000..b5d2565e0
--- /dev/null
+++ b/packages/uni_app/lib/view/current_account/current_account.dart
@@ -0,0 +1,243 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:uni/generated/l10n.dart';
+import 'package:uni/model/entities/current_account.dart';
+import 'package:uni/model/providers/riverpod/current_account_provider.dart';
+import 'package:uni/model/providers/riverpod/profile_provider.dart';
+import 'package:uni/utils/navigation_items.dart';
+import 'package:uni/view/current_account/widgets/account_overview.dart';
+import 'package:uni/view/current_account/widgets/current_account_shimmers.dart';
+import 'package:uni/view/current_account/widgets/no_current_account.dart';
+import 'package:uni/view/current_account/widgets/transaction.dart';
+import 'package:uni/view/current_account/widgets/transaction_filter_menu.dart';
+import 'package:uni/view/widgets/pages_layouts/secondary/secondary.dart';
+
+class CurrentAccountPageView extends ConsumerStatefulWidget {
+ const CurrentAccountPageView({super.key});
+
+ @override
+ ConsumerState createState() =>
+ CurrentAccountPageViewState();
+}
+
+class CurrentAccountPageViewState
+ extends SecondaryPageViewState {
+ String _selectedFilter = 'Pending';
+
+ @override
+ String? getTitle() =>
+ S.of(context).nav_title(NavigationItem.navCurrentAccount.route);
+
+ @override
+ Future onRefresh() async {}
+
+ List _filterTuition(
+ List unpaid,
+ List statement,
+ ) {
+ final List combined =
+ [
+ ...unpaid.where(
+ (e) => e.description.toLowerCase().contains('propina'),
+ ),
+ ...statement.where(
+ (e) => e.description.toLowerCase().contains('propina'),
+ ),
+ ]..sort((a, b) {
+ final dateA = (a is Unpaid)
+ ? (a.deadline ?? a.date)
+ : (a as AccountStatement).date;
+ final dateB = (b is Unpaid)
+ ? (b.deadline ?? b.date)
+ : (b as AccountStatement).date;
+ return dateB.compareTo(dateA);
+ });
+
+ return combined;
+ }
+
+ Widget _buildListView(List items) {
+ if (items.isEmpty) {
+ return const Center(child: Text('Sem registos para este filtro.'));
+ }
+
+ return ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
+ itemCount: items.length,
+ separatorBuilder: (context, index) => const SizedBox(height: 12),
+ itemBuilder: (context, index) {
+ final item = items[index];
+
+ if (item is Unpaid) {
+ return Transaction(
+ description: item.description,
+ date: item.date,
+ deadline: item.deadline,
+ value: item.amountDue,
+ interestOnLatePayment: item.interestOnLatePayment,
+ paymentLink: item.paymentLink,
+ isUnpaid: true,
+ );
+ } else if (item is AccountStatement) {
+ return Transaction(
+ description: item.description,
+ date: item.date,
+ value: item.credit,
+ );
+ }
+ return const SizedBox.shrink();
+ },
+ );
+ }
+
+ @override
+ Widget getBody(BuildContext context) {
+ final currentAccount = ref.watch(currentAccountProvider);
+ final accountOverview = ref.watch(profileProvider);
+
+ if (currentAccount.isLoading || accountOverview.isLoading) {
+ return const CurrentAccountShimmers();
+ }
+
+ if (currentAccount.hasError || accountOverview.hasError) {
+ return LayoutBuilder(
+ builder: (context, constraints) => SingleChildScrollView(
+ physics: const AlwaysScrollableScrollPhysics(),
+ child: Container(
+ height: constraints.maxHeight,
+ padding: const EdgeInsets.only(bottom: 120),
+ child: Center(
+ child: CurrentAccountNoInfo(
+ label: S.of(context).no_info,
+ sublabel: S.of(context).no_current_account_info,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ final unpaid = currentAccount.value!.$1;
+ final history = currentAccount.value!.$2;
+
+ final nextItem = unpaid.isNotEmpty ? unpaid.first : null;
+
+ final p = accountOverview.value;
+
+ List currentList;
+
+ switch (_selectedFilter) {
+ case 'Pending':
+ currentList = unpaid;
+ case 'Tuition Fees':
+ currentList = _filterTuition(unpaid, history);
+ case 'General History':
+ currentList = history;
+ default:
+ currentList = [];
+ }
+
+ return ListView(
+ padding: const EdgeInsets.symmetric(vertical: 20),
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Text(
+ S.of(context).overview,
+ style: Theme.of(context).textTheme.headlineLarge,
+ ),
+ ),
+ const SizedBox(height: 8),
+ AccountOverview(
+ feesBalance: p!.feesBalance,
+ feesLimit: p.feesLimit,
+ printBalance: p.printBalance,
+ ),
+ const SizedBox(height: 8),
+ if (nextItem != null) ...[
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Text(
+ S.of(context).upcoming_due,
+ style: Theme.of(context).textTheme.headlineLarge,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Transaction(
+ description: nextItem.description,
+ date: nextItem.date,
+ deadline: nextItem.deadline,
+ value: nextItem.amountDue,
+ interestOnLatePayment: nextItem.interestOnLatePayment,
+ paymentLink: nextItem.paymentLink,
+ isUnpaid: true,
+ ),
+ ),
+ ],
+ const SizedBox(height: 22),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Text(
+ S.of(context).transactions,
+ style: Theme.of(context).textTheme.headlineLarge,
+ ),
+ ),
+ Container(
+ margin: const EdgeInsets.only(right: 20),
+ child: TransactionFilterMenu(
+ items: const ['Pending', 'Tuition Fees', 'General History'],
+ selectedValue: _selectedFilter,
+ onSelectionChanged: (newValue) {
+ setState(() {
+ _selectedFilter = newValue;
+ });
+ },
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ if (currentList.isEmpty)
+ switch (_selectedFilter) {
+ 'Pending' => Transform.scale(
+ scale: 0.8,
+ child: CurrentAccountNoInfo(
+ label: S.of(context).no_pending_label,
+ sublabel: S.of(context).no_pending_sublabel,
+ ),
+ ),
+ 'Tuition Fees' => Transform.scale(
+ scale: 0.8,
+ child: CurrentAccountNoInfo(
+ label: S.of(context).no_tuition_fees_label,
+ sublabel: S.of(context).no_tuition_fees_sublabel,
+ ),
+ ),
+ 'General History' => Transform.scale(
+ scale: 0.8,
+ child: CurrentAccountNoInfo(
+ label: S.of(context).no_history_label,
+ sublabel: S.of(context).no_history_sublabel,
+ ),
+ ),
+ _ => Transform.scale(
+ scale: 0.8,
+ child: CurrentAccountNoInfo(
+ label: S.of(context).no_pending_label,
+ sublabel: S.of(context).no_pending_sublabel,
+ ),
+ ),
+ }
+ else
+ _buildListView(currentList),
+ ],
+ );
+ }
+}
diff --git a/packages/uni_app/lib/view/current_account/widgets/account_overview.dart b/packages/uni_app/lib/view/current_account/widgets/account_overview.dart
new file mode 100644
index 000000000..e0ec77b56
--- /dev/null
+++ b/packages/uni_app/lib/view/current_account/widgets/account_overview.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import 'package:uni/generated/l10n.dart';
+import 'package:uni_ui/cards/generic_card.dart';
+import 'package:uni_ui/cards/profile_list_tile.dart';
+import 'package:uni_ui/icons.dart';
+
+class AccountOverview extends StatelessWidget {
+ const AccountOverview({
+ super.key,
+ required this.feesBalance,
+ this.feesLimit,
+ required this.printBalance,
+ });
+
+ final String feesBalance;
+ final DateTime? feesLimit;
+ final String printBalance;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ GenericCard(
+ tooltip: S.of(context).balance,
+ margin: const EdgeInsets.only(bottom: 14, right: 20, left: 20),
+ padding: EdgeInsets.zero,
+
+ child: ProfileListTile(
+ icon: UniIcons.piggyBank,
+ title: S.of(context).balance,
+ subtitle: S.of(context).balance_description,
+ trailing: Text(
+ feesBalance,
+ style: Theme.of(context).textTheme.headlineSmall,
+ ),
+ ),
+ ),
+ GenericCard(
+ tooltip: S.of(context).fee_date,
+ margin: const EdgeInsets.only(bottom: 14, right: 20, left: 20),
+ padding: EdgeInsets.zero,
+
+ child: ProfileListTile(
+ icon: UniIcons.calendarDots,
+ title: S.of(context).fee_date,
+ subtitle: S.of(context).fee_date_description,
+ trailing: Text(
+ feesLimit != null
+ ? DateFormat('yyyy-MM-dd').format(feesLimit!)
+ : S.of(context).no_date,
+ style: Theme.of(context).textTheme.headlineSmall,
+ ),
+ ),
+ ),
+ GenericCard(
+ tooltip: S.of(context).print_balance,
+ margin: const EdgeInsets.only(bottom: 14, right: 20, left: 20),
+ padding: EdgeInsets.zero,
+ child: ProfileListTile(
+ icon: UniIcons.printer,
+ title: S.of(context).print_balance,
+ subtitle: S.of(context).print_balance_description,
+ trailing: Text(
+ printBalance,
+ style: Theme.of(context).textTheme.headlineSmall,
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/packages/uni_app/lib/view/current_account/widgets/current_account_shimmers.dart b/packages/uni_app/lib/view/current_account/widgets/current_account_shimmers.dart
new file mode 100644
index 000000000..a9818fc36
--- /dev/null
+++ b/packages/uni_app/lib/view/current_account/widgets/current_account_shimmers.dart
@@ -0,0 +1,131 @@
+import 'package:flutter/material.dart';
+import 'package:shimmer/shimmer.dart';
+
+class CurrentAccountShimmers extends StatelessWidget {
+ const CurrentAccountShimmers({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 30, bottom: 8),
+ child: ClipRSuperellipse(
+ borderRadius: BorderRadiusGeometry.circular(20),
+ child: Shimmer.fromColors(
+ baseColor: Colors.grey[300]!,
+ highlightColor: Colors.grey[100]!,
+ child: Container(
+ height: 40,
+ decoration: const BoxDecoration(color: Colors.white),
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: ClipRSuperellipse(
+ borderRadius: BorderRadiusGeometry.circular(20),
+ child: Shimmer.fromColors(
+ baseColor: Colors.grey[300]!,
+ highlightColor: Colors.grey[100]!,
+ child: Container(
+ height: 60,
+ decoration: const BoxDecoration(color: Colors.white),
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: ClipRSuperellipse(
+ borderRadius: BorderRadiusGeometry.circular(20),
+ child: Shimmer.fromColors(
+ baseColor: Colors.grey[300]!,
+ highlightColor: Colors.grey[100]!,
+ child: Container(
+ height: 70,
+ decoration: const BoxDecoration(color: Colors.white),
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: ClipRSuperellipse(
+ borderRadius: BorderRadiusGeometry.circular(20),
+ child: Shimmer.fromColors(
+ baseColor: Colors.grey[300]!,
+ highlightColor: Colors.grey[100]!,
+ child: Container(
+ height: 70,
+ decoration: const BoxDecoration(color: Colors.white),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 14),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: ClipRSuperellipse(
+ borderRadius: BorderRadiusGeometry.circular(20),
+ child: Shimmer.fromColors(
+ baseColor: Colors.grey[300]!,
+ highlightColor: Colors.grey[100]!,
+ child: Container(
+ height: 40,
+ decoration: const BoxDecoration(color: Colors.white),
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: ClipRSuperellipse(
+ borderRadius: BorderRadiusGeometry.circular(20),
+ child: Shimmer.fromColors(
+ baseColor: Colors.grey[300]!,
+ highlightColor: Colors.grey[100]!,
+ child: Container(
+ height: 100,
+ decoration: const BoxDecoration(color: Colors.white),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 14),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: ClipRSuperellipse(
+ borderRadius: BorderRadiusGeometry.circular(20),
+ child: Shimmer.fromColors(
+ baseColor: Colors.grey[300]!,
+ highlightColor: Colors.grey[100]!,
+ child: Container(
+ height: 40,
+ decoration: const BoxDecoration(color: Colors.white),
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: ClipRSuperellipse(
+ borderRadius: BorderRadiusGeometry.circular(20),
+ child: Shimmer.fromColors(
+ baseColor: Colors.grey[300]!,
+ highlightColor: Colors.grey[100]!,
+ child: Container(
+ height: 150,
+ decoration: const BoxDecoration(color: Colors.white),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/packages/uni_app/lib/view/current_account/widgets/no_current_account.dart b/packages/uni_app/lib/view/current_account/widgets/no_current_account.dart
new file mode 100644
index 000000000..14ef1525b
--- /dev/null
+++ b/packages/uni_app/lib/view/current_account/widgets/no_current_account.dart
@@ -0,0 +1,28 @@
+import 'package:flutter/material.dart';
+import 'package:uni/view/widgets/expanded_image_label.dart';
+
+class CurrentAccountNoInfo extends StatelessWidget {
+ const CurrentAccountNoInfo({
+ super.key,
+ required this.label,
+ required this.sublabel,
+ });
+
+ final String label;
+ final String sublabel;
+
+ @override
+ Widget build(BuildContext context) {
+ return ImageLabel(
+ imagePath: 'assets/images/current_account.png',
+ label: label,
+ labelTextStyle: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 18,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ sublabel: sublabel,
+ sublabelTextStyle: Theme.of(context).textTheme.bodyLarge,
+ );
+ }
+}
diff --git a/packages/uni_app/lib/view/current_account/widgets/payment_webview.dart b/packages/uni_app/lib/view/current_account/widgets/payment_webview.dart
new file mode 100644
index 000000000..1fcce999a
--- /dev/null
+++ b/packages/uni_app/lib/view/current_account/widgets/payment_webview.dart
@@ -0,0 +1,90 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_inappwebview/flutter_inappwebview.dart';
+import 'package:uni/session/flows/base/session.dart';
+
+class PaymentWebView extends StatefulWidget {
+ const PaymentWebView({super.key, required this.url, required this.session});
+
+ final String url;
+ final Session session;
+
+ @override
+ State createState() => _PaymentWebViewState();
+}
+
+class _PaymentWebViewState extends State {
+ bool isLoading = true;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Container(
+ margin: const EdgeInsets.all(8),
+ height: 4,
+ width: 40,
+ decoration: BoxDecoration(
+ color: Colors.grey[300],
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ Expanded(
+ child: Stack(
+ children: [
+ InAppWebView(
+ initialUrlRequest: URLRequest(url: WebUri(widget.url)),
+ initialSettings: InAppWebViewSettings(),
+ gestureRecognizers: const {
+ Factory(
+ VerticalDragGestureRecognizer.new,
+ ),
+ },
+ onWebViewCreated: (controller) async {
+ final cookieManager = CookieManager.instance();
+ const baseUrl = 'https://sigarra.up.pt';
+
+ for (final cookie in widget.session.cookies) {
+ await cookieManager.setCookie(
+ url: WebUri(baseUrl),
+ name: cookie.name,
+ value: cookie.value,
+ domain: cookie.domain ?? 'sigarra.up.pt',
+ path: cookie.path ?? '/',
+ isSecure: cookie.secure,
+ isHttpOnly: cookie.httpOnly,
+ );
+ }
+ },
+ onLoadStop: (controller, url) async {
+ final urlString = url?.toString() ?? '';
+
+ if (urlString.contains('https://www.up')) {
+ setState(() {
+ isLoading = false;
+ });
+ return;
+ }
+
+ await controller.evaluateJavascript(
+ source: """
+ const btn = document.getElementById('botao-mb');
+ if (btn) btn.click();
+ """,
+ );
+ },
+ ),
+
+ if (isLoading)
+ Container(
+ color: Colors.white,
+ child: const Center(child: CircularProgressIndicator()),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/packages/uni_app/lib/view/current_account/widgets/transaction.dart b/packages/uni_app/lib/view/current_account/widgets/transaction.dart
new file mode 100644
index 000000000..372ba70af
--- /dev/null
+++ b/packages/uni_app/lib/view/current_account/widgets/transaction.dart
@@ -0,0 +1,184 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:uni/generated/l10n.dart';
+import 'package:uni/model/providers/riverpod/session_provider.dart';
+import 'package:uni/view/current_account/widgets/payment_webview.dart';
+import 'package:uni_ui/icons.dart';
+import 'package:uni_ui/theme.dart';
+
+enum PaymentStatus { paid, pending, overdue }
+
+class Transaction extends ConsumerWidget {
+ const Transaction({
+ super.key,
+ required this.description,
+ required this.date,
+ this.deadline,
+ required this.value,
+ this.interestOnLatePayment,
+ this.isUnpaid = false,
+ this.paymentLink,
+ });
+
+ final String description;
+ final DateTime date;
+ final DateTime? deadline;
+ final double value;
+ final double? interestOnLatePayment;
+ final bool isUnpaid;
+ final String? paymentLink;
+
+ PaymentStatus get status {
+ if (deadline == null) {
+ return isUnpaid ? PaymentStatus.pending : PaymentStatus.paid;
+ }
+
+ final now = DateTime.now();
+ final today = DateTime(now.year, now.month, now.day);
+
+ if (deadline!.isBefore(today)) {
+ return PaymentStatus.overdue;
+ }
+ return PaymentStatus.pending;
+ }
+
+ Color _dotColor(BuildContext context) => switch (status) {
+ PaymentStatus.paid => BadgeColors.pl,
+ PaymentStatus.pending => BadgeColors.ee,
+ PaymentStatus.overdue => BadgeColors.er,
+ };
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final sessionAsync = ref.watch(sessionProvider);
+
+ return Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: ShapeDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainer,
+ shadows: [BoxShadow(color: Colors.black.withValues(alpha: 0.03))],
+ shape: RoundedSuperellipseBorder(
+ borderRadius: BorderRadius.circular(20),
+ ),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: paymentLink != null
+ ? CrossAxisAlignment.start
+ : CrossAxisAlignment.center,
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 12,
+ height: 12,
+ decoration: BoxDecoration(
+ color: _dotColor(context),
+ borderRadius: BorderRadius.circular(5),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ description,
+ style: Theme.of(context).textTheme.headlineSmall,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ status == PaymentStatus.paid || deadline == null
+ ? "${date.day.toString().padLeft(2, '0')}-${date.month.toString().padLeft(2, '0')}-${date.year}"
+ : "${S.of(context).due_in} ${deadline!.day.toString().padLeft(2, '0')}-${deadline!.month.toString().padLeft(2, '0')}-${deadline!.year}",
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(width: 24),
+ IntrinsicHeight(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ if (paymentLink != null)
+ Align(
+ alignment: Alignment.centerRight,
+ child: IconButton(
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ icon: Icon(
+ UniIcons.arrowSquareOut,
+ color: Theme.of(context).colorScheme.primary,
+ size: 18,
+ ),
+ onPressed: () {
+ sessionAsync.whenData((session) {
+ if (session != null) {
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ backgroundColor: Colors.transparent,
+ builder: (context) => Container(
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(20),
+ ),
+ ),
+ clipBehavior: Clip.antiAliasWithSaveLayer,
+ height: MediaQuery.sizeOf(context).height * 0.9,
+ child: PaymentWebView(
+ url: paymentLink!,
+ session: session,
+ ),
+ ),
+ );
+ } else {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(S.of(context).failed_login),
+ ),
+ );
+ }
+ });
+ },
+ visualDensity: VisualDensity.compact,
+ ),
+ ),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ '${(value / 100).toStringAsFixed(2)}€',
+ style: Theme.of(context).textTheme.headlineSmall,
+ ),
+ if (status == PaymentStatus.overdue &&
+ interestOnLatePayment != null) ...[
+ Text(
+ '+ ${(interestOnLatePayment! / 100).toStringAsFixed(2)}€',
+ style: TextStyle(
+ fontSize: 10,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ Text(
+ S.of(context).interest_on_late_payments,
+ style: TextStyle(
+ fontSize: 8,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/packages/uni_app/lib/view/current_account/widgets/transaction_filter_menu.dart b/packages/uni_app/lib/view/current_account/widgets/transaction_filter_menu.dart
new file mode 100644
index 000000000..0907502ea
--- /dev/null
+++ b/packages/uni_app/lib/view/current_account/widgets/transaction_filter_menu.dart
@@ -0,0 +1,175 @@
+import 'package:flutter/material.dart';
+import 'package:uni/generated/l10n.dart';
+import 'package:uni_ui/modal/modal.dart';
+
+class TransactionFilterMenu extends StatefulWidget {
+ const TransactionFilterMenu({
+ super.key,
+ required this.items,
+ required this.selectedValue,
+ required this.onSelectionChanged,
+ });
+
+ final List items;
+ final String selectedValue;
+ final void Function(String) onSelectionChanged;
+
+ @override
+ State createState() => _TransactionFilterMenuState();
+}
+
+class _TransactionFilterMenuState extends State {
+ late String currentSelected;
+
+ @override
+ void initState() {
+ super.initState();
+ currentSelected = widget.selectedValue;
+ }
+
+ @override
+ void didUpdateWidget(TransactionFilterMenu oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.selectedValue != widget.selectedValue) {
+ currentSelected = widget.selectedValue;
+ }
+ }
+
+ void _showFilterDialog(BuildContext context) {
+ String dialogSelected = currentSelected;
+
+ showDialog(
+ context: context,
+ builder: (context) {
+ return StatefulBuilder(
+ builder: (context, setModalState) {
+ return ModalDialog(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(12),
+ child: Text(
+ S.of(context).transactions,
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ const Divider(height: 1),
+ const SizedBox(height: 8),
+
+ ...widget.items.map((keyLabel) {
+ final isSelected = dialogSelected == keyLabel;
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 1),
+ child: ListTile(
+ dense: true,
+ visualDensity: const VisualDensity(vertical: -2),
+ selected: isSelected,
+ selectedTileColor: Theme.of(
+ context,
+ ).colorScheme.primary.withAlpha(20),
+ title: Text(
+ _getFilterLabel(context, keyLabel),
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ fontWeight: isSelected
+ ? FontWeight.bold
+ : FontWeight.normal,
+ color: isSelected
+ ? Theme.of(context).colorScheme.primary
+ : null,
+ ),
+ ),
+ trailing: isSelected
+ ? Icon(
+ Icons.check,
+ color: Theme.of(context).colorScheme.primary,
+ )
+ : null,
+ onTap: () {
+ setModalState(() {
+ dialogSelected = keyLabel;
+ });
+ },
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ ),
+ ),
+ );
+ }),
+
+ const SizedBox(height: 16),
+
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text(
+ S.of(context).cancel,
+ style: const TextStyle(fontSize: 12),
+ ),
+ ),
+ const SizedBox(width: 8),
+ FilledButton(
+ onPressed: () {
+ setState(() {
+ currentSelected = dialogSelected;
+ });
+ widget.onSelectionChanged(currentSelected);
+ Navigator.of(context).pop();
+ },
+ child: Text(
+ S.of(context).apply,
+ style: const TextStyle(
+ fontSize: 12,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ );
+ },
+ );
+ },
+ );
+ }
+
+ String _getFilterLabel(BuildContext context, String key) {
+ switch (key) {
+ case 'Pending':
+ return S.of(context).pending;
+ case 'Tuition Fees':
+ return S.of(context).tuition_fees;
+ case 'General History':
+ return S.of(context).general_history;
+ default:
+ return key;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return TextButton(
+ style: TextButton.styleFrom(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
+ shape: const StadiumBorder(),
+ ),
+ onPressed: () => _showFilterDialog(context),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ _getFilterLabel(context, currentSelected),
+ style: Theme.of(context).textTheme.bodySmall,
+ ),
+ const SizedBox(width: 4),
+ const Icon(Icons.keyboard_arrow_down, size: 16),
+ ],
+ ),
+ );
+ }
+}
diff --git a/packages/uni_app/lib/view/profile/profile.dart b/packages/uni_app/lib/view/profile/profile.dart
index f9606c7fb..6cf120aa3 100644
--- a/packages/uni_app/lib/view/profile/profile.dart
+++ b/packages/uni_app/lib/view/profile/profile.dart
@@ -1,13 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:uni/generated/l10n.dart';
import 'package:uni/model/entities/profile.dart';
import 'package:uni/model/providers/riverpod/default_consumer.dart';
import 'package:uni/model/providers/riverpod/profile_provider.dart';
+import 'package:uni/utils/navigation_items.dart';
import 'package:uni/view/profile/profile_shimmer.dart';
-import 'package:uni/view/profile/widgets/profile_info.dart';
import 'package:uni/view/profile/widgets/profile_overview.dart';
import 'package:uni/view/profile/widgets/settings.dart';
import 'package:uni/view/widgets/pages_layouts/secondary/secondary.dart';
+import 'package:uni_ui/cards/generic_card.dart';
+import 'package:uni_ui/cards/profile_list_tile.dart';
+import 'package:uni_ui/icons.dart';
class ProfilePageView extends ConsumerStatefulWidget {
const ProfilePageView({super.key});
@@ -24,17 +28,35 @@ class ProfilePageViewState extends SecondaryPageViewState {
children: [
DefaultConsumer(
provider: profileProvider,
- builder: (context, ref, profile) => Column(
- children: [
- ProfileOverview(profile: profile),
- const ProfileInfo(),
- ],
- ),
+ builder: (context, ref, profile) => ProfileOverview(profile: profile),
hasContent: (profile) => profile.courses.isNotEmpty,
loadingWidget: const ProfileCardShimmer(),
nullContentWidget: Container(),
),
-
+ GenericCard(
+ tooltip: S.of(context).current_account,
+ margin: const EdgeInsets.only(
+ top: 20,
+ left: 20,
+ right: 20,
+ bottom: 10,
+ ),
+ child: ProfileListTile(
+ icon: UniIcons.bank,
+ title: S.of(context).current_account,
+ subtitle: S.of(context).current_account_description,
+ trailing: UniIcon(
+ UniIcons.caretRight,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ onTap: () {
+ Navigator.pushNamed(
+ context,
+ '/${NavigationItem.navCurrentAccount.route}',
+ );
+ },
+ ),
+ ),
const Settings(),
],
);
diff --git a/packages/uni_app/pubspec.yaml b/packages/uni_app/pubspec.yaml
index ed70214da..6f36f2ff1 100644
--- a/packages/uni_app/pubspec.yaml
+++ b/packages/uni_app/pubspec.yaml
@@ -38,6 +38,7 @@ dependencies:
sdk: flutter
flutter_cache_manager: ^3.4.1
flutter_dotenv: ^6.0.0
+ flutter_inappwebview: ^6.0.0
flutter_local_notifications: ^20.0.0
flutter_localizations:
sdk: flutter
@@ -87,6 +88,7 @@ dependencies:
path: workmanager
ref: "4ce065"
+
dev_dependencies:
build_runner: ^2.7.1
custom_lint: ^0.8.1
diff --git a/packages/uni_app/windows/flutter/generated_plugins.cmake b/packages/uni_app/windows/flutter/generated_plugins.cmake
index ef31d9dc3..4004491be 100644
--- a/packages/uni_app/windows/flutter/generated_plugins.cmake
+++ b/packages/uni_app/windows/flutter/generated_plugins.cmake
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
battery_plus
connectivity_plus
file_selector_windows
+ flutter_inappwebview_windows
flutter_secure_storage_windows
objectbox_flutter_libs
sentry_flutter
diff --git a/packages/uni_ui/lib/icons.dart b/packages/uni_ui/lib/icons.dart
index 662e5c79a..271752405 100644
--- a/packages/uni_ui/lib/icons.dart
+++ b/packages/uni_ui/lib/icons.dart
@@ -94,6 +94,11 @@ class UniIcons {
static const courseUnit = PhosphorIconsDuotone.chalkboardTeacher;
static const warning = PhosphorIconsDuotone.warningOctagon;
+
+ // current account icons
+ static const piggyBank = PhosphorIconsDuotone.piggyBank;
+ static const calendarDots = PhosphorIconsDuotone.calendarDots;
+ static const bank = PhosphorIconsDuotone.bank;
}
// The same as default Icon class from material.dart but allowing to use PhosphorIcons duotone icons
diff --git a/pubspec.lock b/pubspec.lock
index ed88702b7..7f10d028d 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -575,6 +575,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
+ flutter_inappwebview:
+ dependency: transitive
+ description:
+ name: flutter_inappwebview
+ sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.1.5"
+ flutter_inappwebview_android:
+ dependency: transitive
+ description:
+ name: flutter_inappwebview_android
+ sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.3"
+ flutter_inappwebview_internal_annotations:
+ dependency: transitive
+ description:
+ name: flutter_inappwebview_internal_annotations
+ sha256: e30fba942e3debea7b7e6cdd4f0f59ce89dd403a9865193e3221293b6d1544c6
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ flutter_inappwebview_ios:
+ dependency: transitive
+ description:
+ name: flutter_inappwebview_ios
+ sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ flutter_inappwebview_macos:
+ dependency: transitive
+ description:
+ name: flutter_inappwebview_macos
+ sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ flutter_inappwebview_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_inappwebview_platform_interface
+ sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0+1"
+ flutter_inappwebview_web:
+ dependency: transitive
+ description:
+ name: flutter_inappwebview_web
+ sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ flutter_inappwebview_windows:
+ dependency: transitive
+ description:
+ name: flutter_inappwebview_windows
+ sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.0"
flutter_launcher_icons:
dependency: transitive
description:
@@ -1026,10 +1090,10 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
+ sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
- version: "0.12.19"
+ version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
@@ -1720,26 +1784,26 @@ packages:
dependency: transitive
description:
name: test
- sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
+ sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
url: "https://pub.dev"
source: hosted
- version: "1.30.0"
+ version: "1.29.0"
test_api:
dependency: transitive
description:
name: test_api
- sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
+ sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
- version: "0.7.10"
+ version: "0.7.9"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
+ sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
url: "https://pub.dev"
source: hosted
- version: "0.6.16"
+ version: "0.6.15"
timelines:
dependency: transitive
description: