fix(ui): make student profile modal and settings screen responsive for Android and narrow screens

This commit is contained in:
ssbg04 committed 2026-08-01 13:44:18 +08:00
1 parent 7a2cacee15
commit dea1f53a7b
2 files changed
+252 -112

No files matched your search

@@ -85,6 +85,17 @@ class _StudentProfileDialogShellState
final students = pageState.value?.students ?? [];
final currentIndex = students.indexWhere((s) => s.id == _currentStudentId);
final isMobileOrAndroid = widget.isMobile ||
Theme.of(context).platform == TargetPlatform.android;
final arrowIconSize = isMobileOrAndroid ? 20.0 : 16.0;
final actionIconSize = isMobileOrAndroid ? 20.0 : 18.0;
final buttonConstraints = isMobileOrAndroid
? const BoxConstraints(minWidth: 36, minHeight: 36)
: const BoxConstraints(minWidth: 28, minHeight: 28);
final buttonPadding = isMobileOrAndroid
? const EdgeInsets.all(4)
: EdgeInsets.zero;
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: widget.isMobile
@@ -124,6 +135,8 @@ class _StudentProfileDialogShellState
fontSize: 16,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
if (students.isNotEmpty && currentIndex != -1) ...[
@@ -135,19 +148,16 @@ class _StudentProfileDialogShellState
color: currentIndex > 0
? Colors.white
: Colors.white24,
size: 16,
size: arrowIconSize,
),
visualDensity: VisualDensity.compact,
style: IconButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: Size.zero,
padding: EdgeInsets.zero,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 28,
minHeight: 28,
padding: buttonPadding,
),
padding: buttonPadding,
constraints: buttonConstraints,
onPressed: currentIndex > 0
? () => setState(() {
_currentStudentId =
@@ -160,9 +170,9 @@ class _StudentProfileDialogShellState
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
'${currentIndex + 1}/${students.length}',
style: const TextStyle(
style: TextStyle(
color: Colors.white70,
fontSize: 12,
fontSize: isMobileOrAndroid ? 13 : 12,
fontWeight: FontWeight.w600,
),
),
@@ -175,19 +185,16 @@ class _StudentProfileDialogShellState
color: currentIndex < students.length - 1
? Colors.white
: Colors.white24,
size: 16,
size: arrowIconSize,
),
visualDensity: VisualDensity.compact,
style: IconButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: Size.zero,
padding: EdgeInsets.zero,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 28,
minHeight: 28,
padding: buttonPadding,
),
padding: buttonPadding,
constraints: buttonConstraints,
onPressed: currentIndex < students.length - 1
? () => setState(() {
_currentStudentId =
@@ -196,25 +203,25 @@ class _StudentProfileDialogShellState
: null,
),
),
const SizedBox(width: 10),
SizedBox(width: isMobileOrAndroid ? 4 : 10),
],
if ((widget.onEdit != null || widget.onEditById != null) &&
widget.userRole != 'teacher')
IconButton(
icon: const Icon(
icon: Icon(
Icons.edit,
color: Colors.white,
size: 18,
size: actionIconSize,
),
tooltip: 'Edit Student',
visualDensity: VisualDensity.compact,
style: IconButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: Size.zero,
padding: EdgeInsets.zero,
padding: buttonPadding,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
padding: buttonPadding,
constraints: buttonConstraints,
onPressed: () {
if (widget.onEditById != null) {
widget.onEditById!(_currentStudentId);
@@ -225,25 +232,25 @@ class _StudentProfileDialogShellState
),
if ((widget.onEdit != null || widget.onEditById != null) &&
widget.userRole != 'teacher')
const SizedBox(width: 8),
SizedBox(width: isMobileOrAndroid ? 4 : 8),
if ((widget.onDelete != null ||
widget.onDeleteById != null) &&
widget.userRole != 'teacher')
IconButton(
icon: const Icon(
icon: Icon(
Icons.delete,
color: Colors.white,
size: 18,
size: actionIconSize,
),
tooltip: 'Delete Student',
visualDensity: VisualDensity.compact,
style: IconButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: Size.zero,
padding: EdgeInsets.zero,
padding: buttonPadding,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
padding: buttonPadding,
constraints: buttonConstraints,
onPressed: () {
if (widget.onDeleteById != null) {
widget.onDeleteById!(_currentStudentId);
@@ -255,21 +262,21 @@ class _StudentProfileDialogShellState
if ((widget.onDelete != null ||
widget.onDeleteById != null) &&
widget.userRole != 'teacher')
const SizedBox(width: 8),
SizedBox(width: isMobileOrAndroid ? 4 : 8),
IconButton(
icon: const Icon(
icon: Icon(
Icons.close,
color: Colors.white,
size: 18,
size: actionIconSize,
),
visualDensity: VisualDensity.compact,
style: IconButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: Size.zero,
padding: EdgeInsets.zero,
padding: buttonPadding,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
padding: buttonPadding,
constraints: buttonConstraints,
onPressed: () => Navigator.of(context).pop(),
),
],
@@ -338,57 +345,95 @@ class StudentProfileModalBody extends ConsumerWidget {
const SizedBox(height: 20),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Enrollments',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
if (!hideEnrollmentActions &&
(userRole == 'admin' || userRole == 'super_admin'))
Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton.icon(
onPressed: () => _scanSF10SF9(context, ref, studentId),
icon: const Icon(Icons.document_scanner, size: 16),
label: const Text('Scan SF10 / SF9'),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
const SizedBox(width: 8),
TextButton.icon(
onPressed: () {
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.45),
builder: (ctx) => EditEnrollmentModal(
studentId: studentId,
enrollment: null,
Builder(
builder: (ctx) {
final screenW = MediaQuery.of(ctx).size.width;
final isStacked = screenW < 550 ||
Theme.of(ctx).platform == TargetPlatform.android;
final actions = (!hideEnrollmentActions &&
(userRole == 'admin' || userRole == 'super_admin'))
? [
OutlinedButton.icon(
onPressed: () => _scanSF10SF9(context, ref, studentId),
icon: const Icon(Icons.document_scanner, size: 18),
label: const Text('Scan SF10 / SF9'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryGreen,
side: const BorderSide(
color: AppColors.primaryGreen,
),
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
);
},
icon: const Icon(Icons.add, size: 16),
label: const Text('Add Enrollment'),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
if (isStacked)
const SizedBox(height: 8)
else
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () {
showDialog(
context: context,
barrierColor:
Colors.black.withValues(alpha: 0.45),
builder: (ctx) => EditEnrollmentModal(
studentId: studentId,
enrollment: null,
),
);
},
icon: const Icon(Icons.add, size: 18),
label: const Text('Add Enrollment'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
),
),
]
: <Widget>[];
if (isStacked) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Enrollments',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (actions.isNotEmpty) ...[
const SizedBox(height: 10),
...actions,
],
],
),
],
);
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Enrollments',
style:
TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
if (actions.isNotEmpty)
Row(
mainAxisSize: MainAxisSize.min,
children: actions,
),
],
);
},
),
const SizedBox(height: 10),
@@ -839,14 +839,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Active Academic Year Schedule & Auto-Graduation',
'Academic Year & Auto-Graduation',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Set start day/month and end year/month/day for automatic sequential graduation of Grade 10 & 12 students',
'Configure schedule dates for automatic sequential graduation of Grade 10 & 12 students',
style: TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
@@ -873,7 +873,10 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
Wrap(
spacing: 16,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
@@ -890,12 +893,10 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
),
),
const SizedBox(width: 16),
Text(
'Start Date: ${activeYear.startDate ?? "Not set"}',
style: const TextStyle(fontSize: 13),
),
const SizedBox(width: 16),
Text(
'End Date: ${activeYear.endDate ?? "Not set"}',
style: const TextStyle(fontSize: 13),
@@ -924,35 +925,129 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => showDialog(
context: context,
builder: (_) => AcademicYearFormModal(year: activeYear),
Builder(
builder: (ctx) {
final isNarrow =
MediaQuery.of(ctx).size.width < 650 ||
Theme.of(ctx).platform ==
TargetPlatform.android;
if (isNarrow) {
return Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
OutlinedButton.icon(
onPressed: () => showDialog(
context: context,
builder: (_) =>
AcademicYearFormModal(
year: activeYear,
),
),
icon: const Icon(
Icons.date_range,
),
label: const Text(
'Edit Schedule Dates',
),
style:
OutlinedButton.styleFrom(
padding:
const EdgeInsets.symmetric(
vertical: 14,
),
side: const BorderSide(
color: AppColors
.primaryGreen,
),
),
),
const SizedBox(height: 12),
FilledButton.icon(
onPressed: () =>
_handleRunAutoGraduation(
context,
ref,
),
icon: const Icon(
Icons.school,
),
label: const Text(
'Check / Run Auto-Graduation Now',
),
style:
FilledButton.styleFrom(
backgroundColor:
AppColors
.primaryGreen,
padding:
const EdgeInsets.symmetric(
vertical: 14,
),
),
),
],
);
}
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => showDialog(
context: context,
builder: (_) =>
AcademicYearFormModal(
year: activeYear,
),
),
icon: const Icon(
Icons.date_range,
),
label: const Text(
'Edit Schedule Dates',
),
style:
OutlinedButton.styleFrom(
padding:
const EdgeInsets.symmetric(
vertical: 14,
),
side: const BorderSide(
color: AppColors
.primaryGreen,
),
),
),
),
icon: const Icon(Icons.date_range),
label: const Text('Edit Schedule Dates'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: const BorderSide(color: AppColors.primaryGreen),
const SizedBox(width: 16),
Expanded(
child: FilledButton.icon(
onPressed: () =>
_handleRunAutoGraduation(
context,
ref,
),
icon: const Icon(
Icons.school,
),
label: const Text(
'Check / Run Auto-Graduation Now',
),
style:
FilledButton.styleFrom(
backgroundColor:
AppColors
.primaryGreen,
padding:
const EdgeInsets.symmetric(
vertical: 14,
),
),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: FilledButton.icon(
onPressed: () => _handleRunAutoGraduation(context, ref),
icon: const Icon(Icons.school),
label: const Text('Check / Run Auto-Graduation Now'),
style: FilledButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
),
],
],
);
},
),
],
);