From 47b1821b781d2b9f1788d70c162e176a459975d1 Mon Sep 17 00:00:00 2001 From: ssbg04 Date: Wed, 15 Jul 2026 03:01:38 +0800 Subject: [PATCH] feat: UI improvements and bug fixes - Migrated user management to a 3-step stepper - Redesigned Reports screen with side-by-side charts and KPIs - Fixed timezone date formatting in Dashboard Activity logs - Implemented custom native Android splash screen and removed flutter_native_splash --- .../android/app/src/main/AndroidManifest.xml | 15 +- .../com/teamsb/tis_rms/SplashActivity.kt | 54 + .../src/main/res/layout/activity_splash.xml | 14 + .../src/main/res/values-night-v31/styles.xml | 7 + .../app/src/main/res/values-night/styles.xml | 5 + .../app/src/main/res/values-v31/styles.xml | 7 + .../app/src/main/res/values/styles.xml | 5 + frontend/assets/images/logo.svg | 1091 +++++++++++++++++ frontend/lib/core/utils/date_utils.dart | 14 + .../screens/dashboard/dashboard_screen.dart | 10 +- .../dashboard/recent_activities_screen.dart | 10 +- .../dashboard/user_history_screen.dart | 10 +- .../ui/screens/reports/reports_screen.dart | 207 +++- .../lib/ui/screens/splash/splash_screen.dart | 28 +- frontend/pubspec.lock | 40 - frontend/pubspec.yaml | 8 +- 16 files changed, 1396 insertions(+), 129 deletions(-) create mode 100644 frontend/android/app/src/main/kotlin/com/teamsb/tis_rms/SplashActivity.kt create mode 100644 frontend/android/app/src/main/res/layout/activity_splash.xml create mode 100644 frontend/assets/images/logo.svg diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml index d6b8656..2023733 100644 --- a/frontend/android/app/src/main/AndroidManifest.xml +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,17 @@ android:name="${applicationName}" android:icon="@mipmap/ic_launcher"> + + + + + + + - - - - diff --git a/frontend/android/app/src/main/kotlin/com/teamsb/tis_rms/SplashActivity.kt b/frontend/android/app/src/main/kotlin/com/teamsb/tis_rms/SplashActivity.kt new file mode 100644 index 0000000..73cf842 --- /dev/null +++ b/frontend/android/app/src/main/kotlin/com/teamsb/tis_rms/SplashActivity.kt @@ -0,0 +1,54 @@ +package com.teamsb.tis_rms + +import android.animation.ObjectAnimator +import android.animation.PropertyValuesHolder +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.ImageView +import android.app.Activity + +class SplashActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_splash) + + val logoImageView = findViewById(R.id.splash_logo) + + // Create a pulsing scale animation + val scaleDown = ObjectAnimator.ofPropertyValuesHolder( + logoImageView, + PropertyValuesHolder.ofFloat("scaleX", 0.95f), + PropertyValuesHolder.ofFloat("scaleY", 0.95f) + ) + scaleDown.duration = 400 + scaleDown.repeatCount = 1 + scaleDown.repeatMode = ObjectAnimator.REVERSE + scaleDown.interpolator = AccelerateDecelerateInterpolator() + + val scaleUp = ObjectAnimator.ofPropertyValuesHolder( + logoImageView, + PropertyValuesHolder.ofFloat("scaleX", 1.05f), + PropertyValuesHolder.ofFloat("scaleY", 1.05f) + ) + scaleUp.duration = 400 + scaleUp.repeatCount = 1 + scaleUp.repeatMode = ObjectAnimator.REVERSE + scaleUp.interpolator = AccelerateDecelerateInterpolator() + scaleUp.startDelay = 800 + + scaleDown.start() + scaleUp.start() + + // Proceed to MainActivity after animation finishes + Handler(Looper.getMainLooper()).postDelayed({ + startActivity(Intent(this, MainActivity::class.java)) + finish() + // Disable transition animation to make handoff smoother + overridePendingTransition(0, 0) + }, 1800) + } +} diff --git a/frontend/android/app/src/main/res/layout/activity_splash.xml b/frontend/android/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 0000000..48af31e --- /dev/null +++ b/frontend/android/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/frontend/android/app/src/main/res/values-night-v31/styles.xml b/frontend/android/app/src/main/res/values-night-v31/styles.xml index 9d38747..a95f677 100644 --- a/frontend/android/app/src/main/res/values-night-v31/styles.xml +++ b/frontend/android/app/src/main/res/values-night-v31/styles.xml @@ -16,6 +16,13 @@ running. This Theme is only used starting with V2 of Flutter's Android embedding. --> + + diff --git a/frontend/android/app/src/main/res/values-night/styles.xml b/frontend/android/app/src/main/res/values-night/styles.xml index dbc9ea9..a554e8f 100644 --- a/frontend/android/app/src/main/res/values-night/styles.xml +++ b/frontend/android/app/src/main/res/values-night/styles.xml @@ -16,6 +16,11 @@ running. This Theme is only used starting with V2 of Flutter's Android embedding. --> + + diff --git a/frontend/android/app/src/main/res/values-v31/styles.xml b/frontend/android/app/src/main/res/values-v31/styles.xml index 70292bf..e5674ba 100644 --- a/frontend/android/app/src/main/res/values-v31/styles.xml +++ b/frontend/android/app/src/main/res/values-v31/styles.xml @@ -16,6 +16,13 @@ running. This Theme is only used starting with V2 of Flutter's Android embedding. --> + + diff --git a/frontend/android/app/src/main/res/values/styles.xml b/frontend/android/app/src/main/res/values/styles.xml index 0d1fa8f..3cafa6e 100644 --- a/frontend/android/app/src/main/res/values/styles.xml +++ b/frontend/android/app/src/main/res/values/styles.xml @@ -16,6 +16,11 @@ running. This Theme is only used starting with V2 of Flutter's Android embedding. --> + + diff --git a/frontend/assets/images/logo.svg b/frontend/assets/images/logo.svg new file mode 100644 index 0000000..6a4589b --- /dev/null +++ b/frontend/assets/images/logo.svg @@ -0,0 +1,1091 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/lib/core/utils/date_utils.dart b/frontend/lib/core/utils/date_utils.dart index 1d3cf37..83cea4f 100644 --- a/frontend/lib/core/utils/date_utils.dart +++ b/frontend/lib/core/utils/date_utils.dart @@ -5,6 +5,8 @@ /// being converted to the Philippine Standard Time offset (+08:00). library; +import 'package:intl/intl.dart' as intl; + /// Parses a raw UTC timestamp string from the database and returns a /// [DateTime] adjusted to Philippine Standard Time (UTC+8). /// @@ -58,3 +60,15 @@ String formatDateTime(String raw) { return raw.split('T').first; } } + +/// Returns a formatted date-time string explicitly for Modals in `MMM d, yyyy, hh:mm a` format +/// adjusted to Philippine Standard Time. Example: `Mar 4, 2005, 12:30 PM` +String formatModalDate(String raw) { + if (raw.isEmpty) return ''; + try { + final dt = parseToPht(raw); + return intl.DateFormat('MMM d, yyyy, hh:mm a').format(dt); + } catch (_) { + return raw.split('T').first; + } +} diff --git a/frontend/lib/ui/screens/dashboard/dashboard_screen.dart b/frontend/lib/ui/screens/dashboard/dashboard_screen.dart index 0ff0072..d695603 100644 --- a/frontend/lib/ui/screens/dashboard/dashboard_screen.dart +++ b/frontend/lib/ui/screens/dashboard/dashboard_screen.dart @@ -199,7 +199,7 @@ class _DashboardScreenState extends ConsumerState { context: context, title: note.title, description: note.message, - date: _formatDate(note.createdAt), + date: pht.formatModalDate(note.createdAt), icon: _getNotificationIcon(note.title), actionColor: _getNotificationColor(note.title), ); @@ -836,7 +836,7 @@ class _DashboardScreenState extends ConsumerState { context: context, title: a.entityType.toUpperCase(), description: a.description, - date: _formatDate(a.createdAt), + date: pht.formatModalDate(a.createdAt), performedBy: a.performedBy ?? a.username ?? 'System', action: a.action, actionColor: _actionColor(a.action), @@ -887,7 +887,7 @@ class _DashboardScreenState extends ConsumerState { context: context, title: a.entityType.toUpperCase(), description: a.description, - date: _formatDate(a.createdAt), + date: pht.formatModalDate(a.createdAt), performedBy: a.performedBy ?? a.username ?? 'System', action: a.action, actionColor: _actionColor(a.action), @@ -962,7 +962,7 @@ class _DashboardScreenState extends ConsumerState { context: context, title: 'USER ACTIVITY', description: desc, - date: _formatDate(h.createdAt), + date: pht.formatModalDate(h.createdAt), performedBy: h.performedByName ?? h.performedByUsername ?? 'System', action: h.action, actionColor: _actionColor(h.action), @@ -1015,7 +1015,7 @@ class _DashboardScreenState extends ConsumerState { context: context, title: 'USER ACTIVITY', description: desc, - date: _formatDate(h.createdAt), + date: pht.formatModalDate(h.createdAt), performedBy: h.performedByName ?? h.performedByUsername ?? 'System', action: h.action, actionColor: _actionColor(h.action), diff --git a/frontend/lib/ui/screens/dashboard/recent_activities_screen.dart b/frontend/lib/ui/screens/dashboard/recent_activities_screen.dart index 61f32b1..ca28009 100644 --- a/frontend/lib/ui/screens/dashboard/recent_activities_screen.dart +++ b/frontend/lib/ui/screens/dashboard/recent_activities_screen.dart @@ -350,9 +350,9 @@ class _RecentActivitiesScreenState final a = activities[index]; // Format Date and Time - final DateTime parsedDate = DateTime.tryParse(a.createdAt) ?? DateTime.now(); - // [Month Day, Year] -> MMM dd, yyyy - final dateStr = intl.DateFormat('MMM dd, yyyy').format(parsedDate); + final DateTime parsedDate = pht.parseToPht(a.createdAt); + // [Month Day, Year] -> MMM d, yyyy + final dateStr = intl.DateFormat('MMM d, yyyy').format(parsedDate); // [12-hour format] -> hh:mm a final timeStr = intl.DateFormat('hh:mm a').format(parsedDate); @@ -362,7 +362,7 @@ class _RecentActivitiesScreenState context: context, title: a.entityType.toUpperCase(), description: a.description, - date: '$dateStr $timeStr', + date: pht.formatModalDate(a.createdAt), performedBy: a.performedBy ?? a.username ?? 'System', action: a.action, actionColor: _actionColor(a.action), @@ -405,7 +405,7 @@ class _RecentActivitiesScreenState context: context, title: a.entityType.toUpperCase(), description: a.description, - date: _formatDate(a.createdAt), + date: pht.formatModalDate(a.createdAt), performedBy: a.performedBy ?? a.username ?? 'System', action: a.action, actionColor: _actionColor(a.action), diff --git a/frontend/lib/ui/screens/dashboard/user_history_screen.dart b/frontend/lib/ui/screens/dashboard/user_history_screen.dart index b1bf7ca..86eb4fa 100644 --- a/frontend/lib/ui/screens/dashboard/user_history_screen.dart +++ b/frontend/lib/ui/screens/dashboard/user_history_screen.dart @@ -348,9 +348,9 @@ class _UserHistoryScreenState extends ConsumerState { final h = history[index]; // Format Date and Time - final DateTime parsedDate = DateTime.tryParse(h.createdAt) ?? DateTime.now(); - // [Month Day, Year] -> MMM dd, yyyy - final dateStr = intl.DateFormat('MMM dd, yyyy').format(parsedDate); + final DateTime parsedDate = pht.parseToPht(h.createdAt); + // [Month Day, Year] -> MMM d, yyyy + final dateStr = intl.DateFormat('MMM d, yyyy').format(parsedDate); // [12-hour format] -> hh:mm a final timeStr = intl.DateFormat('hh:mm a').format(parsedDate); @@ -360,7 +360,7 @@ class _UserHistoryScreenState extends ConsumerState { context: context, title: 'USER ACTIVITY', description: 'Action performed on ${h.fullName} (@${h.username})', - date: '$dateStr $timeStr', + date: pht.formatModalDate(h.createdAt), performedBy: h.performedByName ?? h.performedByUsername ?? 'System', action: h.action, actionColor: _actionColor(h.action), @@ -404,7 +404,7 @@ class _UserHistoryScreenState extends ConsumerState { context: context, title: 'USER ACTIVITY', description: 'Action performed on ${h.fullName} (@${h.username})', - date: _formatDate(h.createdAt), + date: pht.formatModalDate(h.createdAt), performedBy: h.performedByName ?? h.performedByUsername ?? 'System', action: h.action, actionColor: _actionColor(h.action), diff --git a/frontend/lib/ui/screens/reports/reports_screen.dart b/frontend/lib/ui/screens/reports/reports_screen.dart index ccdcf74..8c20228 100644 --- a/frontend/lib/ui/screens/reports/reports_screen.dart +++ b/frontend/lib/ui/screens/reports/reports_screen.dart @@ -368,23 +368,40 @@ class _ReportsScreenState extends ConsumerState { data: (data) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 1. Yearly Comparison Chart - _buildYearlyComparisonChart(), - const SizedBox(height: AppSizes.p24), - - // 2. Filter Panel (collapsible) + // 1. Filter Panel (collapsible) _buildFilterPanel(context), const SizedBox(height: AppSizes.p24), - // 3. KPI Cards + // 2. KPI Cards _buildMetricsGrid(data.studentCounts), const SizedBox(height: AppSizes.p24), - - // 4. Row of Missing Documents Chart - _buildMissingDocsChart(data.missingDocsBreakdown), + + // 3. Side-by-Side Charts (Responsive) + LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 1200) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 5, child: _buildYearlyComparisonChart(isDesktop: true)), + const SizedBox(width: AppSizes.p24), + Expanded(flex: 5, child: _buildMissingDocsChart(data.missingDocsBreakdown, isDesktop: true)), + ], + ); + } else { + return Column( + children: [ + _buildYearlyComparisonChart(isDesktop: false), + const SizedBox(height: AppSizes.p24), + _buildMissingDocsChart(data.missingDocsBreakdown, isDesktop: false), + ], + ); + } + }, + ), const SizedBox(height: AppSizes.p24), - // 5. Interactive Student Compliance Table + // 4. Interactive Student Compliance Table _buildComplianceTable(data), ], ), @@ -402,26 +419,6 @@ class _ReportsScreenState extends ConsumerState { Widget _buildTitleAndExportActions(BuildContext context) { final statsAsync = ref.watch(reportStatsProvider); final isDesktop = MediaQuery.of(context).size.width > 900; - - final yearsAsync = ref.watch(academicYearsProvider); - final selectedYearId = ref.watch(selectedAcademicYearIdProvider); - final selectedGrade = ref.watch(selectedGradeLevelProvider); - final selectedSection = ref.watch(selectedSectionIdProvider); - final selectedStatus = ref.watch(selectedStatusFilterProvider); - final sections = ref.watch(filteredSectionsProvider); - - final years = yearsAsync.asData?.value ?? []; - final yearLabel = selectedYearId != null - ? years.firstWhere((y) => y.id == selectedYearId, orElse: () => AcademicYear(id: 0, yearRange: 'Selected', status: '')).yearRange - : 'All Years'; - final gradeLabel = selectedGrade != null ? 'Grade $selectedGrade' : 'All Grades'; - final statusLabel = selectedStatus ?? 'All Statuses'; - - String sectionLabel = 'All Sections'; - if (selectedSection != null && sections.isNotEmpty) { - final s = sections.firstWhere((sec) => (sec['id'] as num).toInt() == selectedSection, orElse: () => {}); - if (s.isNotEmpty) sectionLabel = s['name'] as String; - } return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -750,7 +747,7 @@ class _ReportsScreenState extends ConsumerState { } // ── Missing Documents Breakdown Card ────────────────────────────────────── - Widget _buildMissingDocsChart(List breakdown) { + Widget _buildMissingDocsChart(List breakdown, {required bool isDesktop}) { final isFilterExpanded = ref.watch(missingDocsFilterExpandedProvider); // Categorize by SHS (grade 11-12) vs JHS (grade 7-10) @@ -774,6 +771,7 @@ class _ReportsScreenState extends ConsumerState { return Container( width: double.infinity, + height: isDesktop ? 460 : null, padding: const EdgeInsets.all(AppSizes.p24), decoration: _cardDecoration(), child: Column( @@ -796,36 +794,36 @@ class _ReportsScreenState extends ConsumerState { ), const SizedBox(width: 12), // Show/Hide toggle - TextButton.icon( - onPressed: () => ref.read(missingDocsFilterExpandedProvider.notifier).state = !isFilterExpanded, - icon: Icon(isFilterExpanded ? Icons.visibility_off_outlined : Icons.visibility_outlined, - size: 16, color: AppColors.primaryGreen), - label: Text(isFilterExpanded ? 'Hide' : 'Show', - style: const TextStyle(color: AppColors.primaryGreen, fontSize: 13)), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: const BorderSide(color: AppColors.primaryGreen, width: 0.8), + if (!isDesktop) + TextButton.icon( + onPressed: () => ref.read(missingDocsFilterExpandedProvider.notifier).state = !isFilterExpanded, + icon: Icon(isFilterExpanded ? Icons.visibility_off_outlined : Icons.visibility_outlined, + size: 16, color: AppColors.primaryGreen), + label: Text(isFilterExpanded ? 'Hide' : 'Show', + style: const TextStyle(color: AppColors.primaryGreen, fontSize: 13)), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: const BorderSide(color: AppColors.primaryGreen, width: 0.8), + ), ), ), - ), ], ), + + if (isDesktop) + const SizedBox(height: AppSizes.p24), + // ── Expandable content ────────────────────────────────────────────── - AnimatedCrossFade( - firstChild: const SizedBox.shrink(), - secondChild: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: AppSizes.p24), - if (breakdown.isEmpty) - _emptyWidget('No missing document requirements found. Compliance is 100%!') - else - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: breakdown.length, + if (isDesktop) + Expanded( + child: breakdown.isEmpty + ? _emptyWidget('No missing document requirements found. Compliance is 100%!') + : ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: breakdown.length, separatorBuilder: (context, index) => const SizedBox(height: 10), itemBuilder: (context, index) { final item = breakdown[index]; @@ -897,11 +895,97 @@ class _ReportsScreenState extends ConsumerState { ); }, ), - ], + ) + else + AnimatedCrossFade( + firstChild: const SizedBox.shrink(), + secondChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: AppSizes.p24), + if (breakdown.isEmpty) + _emptyWidget('No missing document requirements found. Compliance is 100%!') + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: breakdown.length, + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final item = breakdown[index]; + final maxCount = breakdown.first.count; + final pct = maxCount > 0 ? item.count / maxCount : 0.0; + final level = detectLevel(item.name); + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Level badge + SizedBox( + width: 34, + child: level == 'SHS' + ? levelBadge('SHS', Colors.purple.shade50, Colors.purple.shade700) + : level == 'JHS' + ? levelBadge('JHS', Colors.teal.shade50, Colors.teal.shade700) + : levelBadge('ALL', Colors.grey.shade100, Colors.grey.shade600), + ), + const SizedBox(width: 8), + // Name + SizedBox( + width: 155, + child: Text(item.name, + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12), + overflow: TextOverflow.ellipsis, + maxLines: 1), + ), + const SizedBox(width: 10), + // Progress bar (compact height) + Expanded( + child: Stack( + children: [ + Container( + height: 10, + decoration: BoxDecoration( + color: Colors.grey.shade100, borderRadius: BorderRadius.circular(5)), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 600), + curve: Curves.easeOut, + height: 10, + width: (MediaQuery.of(context).size.width - 300) * 0.45 * pct, + decoration: BoxDecoration( + gradient: const LinearGradient(colors: [Colors.orange, Colors.redAccent]), + borderRadius: BorderRadius.circular(5), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + // Count badge + Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${item.count}', + style: TextStyle( + color: Colors.red.shade700, + fontWeight: FontWeight.bold, + fontSize: 11), + ), + ), + ], + ); + }, + ), + ], + ), + crossFadeState: isFilterExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 250), ), - crossFadeState: isFilterExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, - duration: const Duration(milliseconds: 250), - ), ], ), ); @@ -1134,7 +1218,7 @@ class _ReportsScreenState extends ConsumerState { ); // ── Yearly Comparison Chart ─────────────────────────────────────────────── - Widget _buildYearlyComparisonChart() { + Widget _buildYearlyComparisonChart({required bool isDesktop}) { final yearlyAsync = ref.watch(yearlyComparisonProvider); final selectedYears = ref.watch(yearlyComparisonSelectedYearsProvider); final selectedStatuses = ref.watch(yearlyComparisonSelectedStatusesProvider); @@ -1149,6 +1233,7 @@ class _ReportsScreenState extends ConsumerState { return Container( width: double.infinity, + height: isDesktop ? 460 : null, padding: const EdgeInsets.all(AppSizes.p24), decoration: _cardDecoration(), child: Column( diff --git a/frontend/lib/ui/screens/splash/splash_screen.dart b/frontend/lib/ui/screens/splash/splash_screen.dart index 8275277..9ffb4c7 100644 --- a/frontend/lib/ui/screens/splash/splash_screen.dart +++ b/frontend/lib/ui/screens/splash/splash_screen.dart @@ -18,15 +18,33 @@ class SplashScreen extends ConsumerStatefulWidget { ConsumerState createState() => _SplashScreenState(); } -class _SplashScreenState extends ConsumerState { +class _SplashScreenState extends ConsumerState with SingleTickerProviderStateMixin { String _statusText = 'Starting up…'; double? _scanProgress; // null = indeterminate, 0.0–1.0 = progress + + late AnimationController _logoAnimController; + late Animation _logoScaleAnimation; @override void initState() { super.initState(); + _logoAnimController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(reverse: true); + + _logoScaleAnimation = Tween(begin: 0.95, end: 1.05).animate( + CurvedAnimation(parent: _logoAnimController, curve: Curves.easeInOut), + ); + _initializeApp(); } + + @override + void dispose() { + _logoAnimController.dispose(); + super.dispose(); + } Future _initializeApp() async { await Future.delayed(const Duration(milliseconds: 800)); @@ -222,7 +240,13 @@ class _SplashScreenState extends ConsumerState { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Image.asset('assets/images/logo.png', width: 150, height: 150), + if (kIsWeb || Platform.isWindows || Platform.isMacOS || Platform.isLinux) + ScaleTransition( + scale: _logoScaleAnimation, + child: Image.asset('assets/images/logo.png', width: 150, height: 150), + ) + else + Image.asset('assets/images/logo.png', width: 150, height: 150), const SizedBox(height: 24), const Text( 'TIS RMS', diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 3cb5f16..5811379 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -17,14 +17,6 @@ packages: url: "https://pub.dev" source: hosted version: "12.1.0" - ansicolor: - dependency: transitive - description: - name: ansicolor - sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" - url: "https://pub.dev" - source: hosted - version: "2.0.3" archive: dependency: transitive description: @@ -193,14 +185,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -382,14 +366,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - flutter_native_splash: - dependency: "direct main" - description: - name: flutter_native_splash - sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" - url: "https://pub.dev" - source: hosted - version: "2.4.4" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -520,14 +496,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - html: - dependency: transitive - description: - name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" - source: hosted - version: "0.15.6" http: dependency: transitive description: @@ -1293,14 +1261,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 - url: "https://pub.dev" - source: hosted - version: "2.3.1" url_launcher: dependency: "direct main" description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 77b132a..24ed577 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -34,7 +34,6 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 - flutter_native_splash: 2.4.4 flutter_riverpod: ^3.3.1 dio: ^5.9.2 flutter_secure_storage: ^10.1.0 @@ -128,12 +127,7 @@ flutter_launcher_icons: image_path: "assets/images/logo.png" min_sdk_android: 21 # Ensures compatibility with older Android devices -flutter_native_splash: - color: "#F9F9F9" # Off-white page background - image: "assets/images/logo.png" - android_12: - image: "assets/images/logo.png" - icon_background_color: "#F9F9F9" + msix_config: display_name: TIS RMS