feat(frontend): green window title bar, login pre-fill, monochrome multi-select, responsive enrollments, and 15 limit archive pagination

This commit is contained in:
ssbg04 committed 2026-08-07 19:57:30 +08:00
1 parent a03f85604e
commit 8e32bbe5e1
10 files changed
+286 -200

No files matched your search

+30 -3
View File
@@ -88,11 +88,34 @@ Files backup/restore - mirror mode or zip -->
<!-- ## backend server backup/restore
- do not use zip and unzip -->
# DASHBOARD SCREEN
- remove background color in icon setup required widget
# DOCUMENT SCREEN
- hold and right click feature for files, add select, when clicked select shows multi select menu
- remove icon before 'Students Folder'
- remove background color in icon files
- enlarge icon slightly
- android app hold feature dropdown same actions copy to menu of files
- pagination limit 20
- add back prevention for multi select menu active, it deactivates multi select menu
- add back prevention for opened student folder, back to student folders tab
# STUDENT SCREEN
- update multi select menu action same from the document screen, add select all
- update multi select menu actions same from the document screen
- add top and bottom list edge fade
- add new student -> student details step -> responsive back and next button
- pagination limit 20
- add back prevention for multi select menu active, it deactivates multi select menu
# ARCHIVES SCREEN
- remove icon before 'System Archives'
- pagination limit 20
- add back prevention for opened student folder, back to student folders tab
# USERS MANAGEMENT SCREEN
- pagination limit 20
<!-- # DOCUMENTS SCREEN
1. apply dark mode
@@ -123,9 +146,13 @@ Windows Service - NSSM nodejs
---
## (dont do) add some features
- archives screen, add files menu for print, download
- copy files name
- auto enrollment OCR do not add in database if not 7-12 grade level detect
- auto add enrollment in add student if the OCR upload is SF9/SF10
- dark mode -> ongoing
- documents screen
- excel to pdf all tabs export into one pdf
- excel to pdf: all tabs export into one pdf
---
@@ -46,7 +46,7 @@ class ArchiveRepository {
Future<ArchivePage> getArchives({
String search = '',
int page = 1,
int limit = 10,
int limit = 15,
String status = 'All Statuses',
}) async {
try {
@@ -95,7 +95,7 @@ class ArchiveRepository {
Future<DocumentPage> getArchivedDocuments({
String search = '',
int page = 1,
int limit = 20,
int limit = 15,
String status = 'All Statuses',
String documentType = 'All Types',
String gradeLevel = '',
+1 -1
View File
@@ -79,7 +79,7 @@ class TisRmsApp extends ConsumerWidget {
navigatorKey: navigatorKey,
child: MaterialApp(
navigatorKey: navigatorKey,
title: 'TIS RMS',
title: 'TIS Record Management System',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
@@ -373,19 +373,25 @@ class _WindowsSidebarLayoutState extends ConsumerState<WindowsSidebarLayout> {
SizedBox(
height: 32,
child: WindowCaption(
brightness: Theme.of(context).brightness,
backgroundColor: Theme.of(context).brightness == Brightness.dark
? Theme.of(context).colorScheme.surface
: AppColors.primaryGreen,
title: Text(
'TIS RMS',
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? Theme.of(context).colorScheme.onSurface
: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
brightness: Brightness.dark,
backgroundColor: AppColors.primaryGreen,
title: Row(
children: [
Image.asset(
'assets/images/logo.png',
width: 18,
height: 18,
),
const SizedBox(width: 8),
const Text(
'TIS Record Management System',
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
@@ -20,7 +20,7 @@ class ArchiveQueryParams {
const ArchiveQueryParams({
this.search = '',
this.page = 1,
this.limit = 10,
this.limit = 15,
this.status = 'All Statuses',
});
@@ -147,7 +147,7 @@ class ArchiveDocumentQueryParams {
const ArchiveDocumentQueryParams({
this.search = '',
this.page = 1,
this.limit = 20,
this.limit = 15,
this.status = 'All Statuses',
this.documentType = 'All Types',
this.gradeLevel = '',
@@ -269,7 +269,12 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
final documentId = document.id;
final studentId = document.studentId;
if (action == 'view_profile' && studentId != null) {
if (action == 'select') {
setState(() {
_isMultiSelectMode = true;
_selectedDocumentIds.add(documentId);
});
} else if (action == 'view_profile' && studentId != null) {
showStudentProfileModal(
context,
studentId: studentId,
@@ -568,6 +573,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
final pageDocs = ref.watch(documentPageProvider).value?.documents ?? [];
final pageIds = pageDocs.map((d) => d.id).toSet();
final bool allSelected = pageIds.isNotEmpty && pageIds.every((id) => _selectedDocumentIds.contains(id));
final buttonColor = isDark ? Colors.white : Colors.black;
return Container(
width: double.infinity,
@@ -582,13 +588,15 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
scrollDirection: Axis.horizontal,
child: Row(
children: [
IconButton(
icon: Icon(Icons.close, color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary),
tooltip: 'Cancel selection',
onPressed: () => setState(() {
_selectedDocumentIds.clear();
_isMultiSelectMode = false;
}),
Tooltip(
message: 'Cancel selection',
child: IconButton(
icon: Icon(Icons.close, color: buttonColor),
onPressed: () => setState(() {
_selectedDocumentIds.clear();
_isMultiSelectMode = false;
}),
),
),
const SizedBox(width: 8),
Text(
@@ -600,72 +608,71 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
),
),
const SizedBox(width: 8),
TextButton.icon(
onPressed: pageDocs.isEmpty
? null
: () {
setState(() {
if (allSelected) {
_selectedDocumentIds.removeAll(pageIds);
if (_selectedDocumentIds.isEmpty) {
_isMultiSelectMode = false;
}
} else {
_selectedDocumentIds.addAll(pageIds);
}
});
},
icon: Icon(
allSelected ? Icons.deselect : Icons.select_all,
size: 18,
color: isDark ? AppColors.darkTextPrimary : AppColors.primaryGreen,
),
label: Text(
allSelected ? 'Deselect All' : 'Select All',
style: TextStyle(
color: isDark ? AppColors.darkTextPrimary : AppColors.primaryGreen,
Tooltip(
message: allSelected ? 'Unselect All' : 'Select All',
child: IconButton(
icon: Icon(
allSelected ? Icons.deselect : Icons.select_all,
color: buttonColor,
),
onPressed: pageDocs.isEmpty
? null
: () {
setState(() {
if (allSelected) {
_selectedDocumentIds.clear();
} else {
_selectedDocumentIds.addAll(pageIds);
}
});
},
),
),
const SizedBox(width: 16),
Container(width: 1, height: 24, color: isDark ? AppColors.darkBorder : Colors.grey.shade400),
const SizedBox(width: 12),
_batchActionBtn(
icon: Icons.print_rounded,
label: 'Print',
color: AppColors.primaryGreen,
onTap: count == 0 ? () {} : _handleBatchPrint,
Tooltip(
message: 'Print',
child: IconButton(
icon: Icon(Icons.print_rounded, color: buttonColor),
onPressed: count == 0 ? null : _handleBatchPrint,
),
),
_batchActionBtn(
icon: Icons.copy_rounded,
label: 'Copy',
color: Colors.blue,
onTap: count == 0 ? () {} : _handleBatchCopy,
Tooltip(
message: 'Copy',
child: IconButton(
icon: Icon(Icons.copy_rounded, color: buttonColor),
onPressed: count == 0 ? null : _handleBatchCopy,
),
),
_batchActionBtn(
icon: Icons.download_rounded,
label: 'Download',
color: AppColors.primaryGreen,
onTap: count == 0 ? () {} : _handleBatchDownload,
Tooltip(
message: 'Download',
child: IconButton(
icon: Icon(Icons.download_rounded, color: buttonColor),
onPressed: count == 0 ? null : _handleBatchDownload,
),
),
_batchActionBtn(
icon: Icons.check_circle_outline_rounded,
label: 'Complete',
color: AppColors.success,
onTap: count == 0 ? () {} : () => _handleBatchStatus('Completed'),
Tooltip(
message: 'Complete',
child: IconButton(
icon: Icon(Icons.check_circle_outline_rounded, color: buttonColor),
onPressed: count == 0 ? null : () => _handleBatchStatus('Completed'),
),
),
_batchActionBtn(
icon: Icons.archive_outlined,
label: 'Archive',
color: Colors.orange,
onTap: count == 0 ? () {} : () => _handleBatchStatus('Archived'),
Tooltip(
message: 'Archive',
child: IconButton(
icon: Icon(Icons.archive_outlined, color: buttonColor),
onPressed: count == 0 ? null : () => _handleBatchStatus('Archived'),
),
),
if (isAdmin)
_batchActionBtn(
icon: Icons.delete_outline_rounded,
label: 'Delete',
color: AppColors.error,
onTap: count == 0 ? () {} : _handleBatchDelete,
Tooltip(
message: 'Delete',
child: IconButton(
icon: Icon(Icons.delete_outline_rounded, color: buttonColor),
onPressed: count == 0 ? null : _handleBatchDelete,
),
),
],
),
@@ -673,46 +680,6 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
);
}
/// Icon + label button used in the mobile batch actions row.
Widget _batchActionBtn({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.25)),
),
child: Icon(icon, color: color, size: 22),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: color,
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
@@ -3079,6 +3046,16 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
Overlay.of(context).context.findRenderObject() as RenderBox;
final items = <PopupMenuEntry<String>>[
const PopupMenuItem(
value: 'select',
child: Row(
children: [
Icon(Icons.check_box_outlined, size: 18),
SizedBox(width: 12),
Text('Select', style: TextStyle(fontSize: 14)),
],
),
),
const PopupMenuItem(
value: 'queue',
child: Row(
@@ -77,6 +77,16 @@ class _FileFolderCardState extends State<FileFolderCard> {
List<PopupMenuEntry<String>> _buildMenuItems() {
final items = <PopupMenuEntry<String>>[
const PopupMenuItem(
value: 'select',
child: Row(
children: [
Icon(Icons.check_box_outlined, size: 18),
SizedBox(width: 12),
Text('Select', style: TextStyle(fontSize: 14)),
],
),
),
const PopupMenuItem(
value: 'queue',
child: Row(
+31 -14
View File
@@ -211,6 +211,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> with SingleTickerProv
showDialog(
context: context,
builder: (ctx) => _ForgotPasswordDialog(
initialUsername: _usernameController.text,
onSuccess: (msg) {
Navigator.pop(ctx);
showSuccessDialog(
@@ -241,19 +242,25 @@ class _LoginScreenState extends ConsumerState<LoginScreen> with SingleTickerProv
SizedBox(
height: 32,
child: WindowCaption(
brightness: Theme.of(context).brightness,
backgroundColor: isDark
? AppColors.primaryGreen
: AppColors.primaryGreen,
title: Text(
'Talisay Integrated School',
style: TextStyle(
color: isDark
? Theme.of(context).colorScheme.onSurface
: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
brightness: Brightness.dark,
backgroundColor: AppColors.primaryGreen,
title: Row(
children: [
Image.asset(
'assets/images/logo.png',
width: 18,
height: 18,
),
const SizedBox(width: 8),
const Text(
'TIS Record Management System',
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
@@ -651,7 +658,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen> with SingleTickerProv
// ================================================================
class _ForgotPasswordDialog extends ConsumerStatefulWidget {
final void Function(String message) onSuccess;
const _ForgotPasswordDialog({required this.onSuccess});
final String? initialUsername;
const _ForgotPasswordDialog({required this.onSuccess, this.initialUsername});
@override
ConsumerState<_ForgotPasswordDialog> createState() =>
@@ -666,6 +674,15 @@ class _ForgotPasswordDialogState extends ConsumerState<_ForgotPasswordDialog> {
bool _obscurePasswords = true;
bool _isLoading = false;
@override
void initState() {
super.initState();
if (widget.initialUsername != null &&
widget.initialUsername!.trim().isNotEmpty) {
_usernameCtrl.text = widget.initialUsername!.trim();
}
}
@override
void dispose() {
_usernameCtrl.dispose();
@@ -264,18 +264,28 @@ class _SplashScreenState extends ConsumerState<SplashScreen>
children: [
if (!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS))
const SizedBox(
SizedBox(
height: 32,
child: WindowCaption(
brightness: Brightness.dark,
backgroundColor: AppColors.primaryGreen,
title: Text(
'TIS RMS',
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
title: Row(
children: [
Image.asset(
'assets/images/logo.png',
width: 18,
height: 18,
),
const SizedBox(width: 8),
const Text(
'TIS Record Management System',
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
@@ -638,67 +638,106 @@ class _EditStudentModalState extends ConsumerState<EditStudentModal> {
(a, b) => (b.gradeLevel ?? 0).compareTo(a.gradeLevel ?? 0),
);
final isMobile = MediaQuery.of(context).size.width < 600;
Widget buildHeaderButtons({required bool expanded}) {
final ocrButton = OutlinedButton.icon(
onPressed: _handleScanEnrollmentFromSF,
icon: const Icon(Icons.document_scanner, size: 18),
label: Text(
isMobile ? 'OCR Scan' : 'OCR Scan (SF9/SF10)',
overflow: TextOverflow.ellipsis,
),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 8 : 14,
vertical: 10,
),
),
);
final addButton = ElevatedButton.icon(
onPressed: () {
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.45),
builder: (ctx) => EditEnrollmentModal(
studentId: widget.student.id,
enrollment: null,
),
).then((_) {
if (mounted) {
ref.invalidate(studentDetailProvider(widget.student.id));
ref.invalidate(studentPageProvider);
}
});
},
icon: const Icon(Icons.add, size: 18),
label: const Text(
'Add Enrollment',
overflow: TextOverflow.ellipsis,
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 8 : 14,
vertical: 10,
),
),
);
if (expanded) {
return Row(
children: [
Expanded(child: ocrButton),
const SizedBox(width: 8),
Expanded(child: addButton),
],
);
} else {
return Row(
children: [
ocrButton,
const SizedBox(width: 8),
addButton,
],
);
}
}
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Academic History (${sorted.length})',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
if (isMobile) ...[
Text(
'Academic History (${sorted.length})',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
),
),
const SizedBox(height: 12),
buildHeaderButtons(expanded: true),
] else ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Academic History (${sorted.length})',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
),
),
),
Row(
children: [
OutlinedButton.icon(
onPressed: _handleScanEnrollmentFromSF,
icon: const Icon(Icons.document_scanner, size: 18),
label: const Text('OCR Scan (SF9/SF10)'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () {
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.45),
builder: (ctx) => EditEnrollmentModal(
studentId: widget.student.id,
enrollment: null,
),
).then((_) {
if (mounted) {
ref.invalidate(studentDetailProvider(widget.student.id));
ref.invalidate(studentPageProvider);
}
});
},
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,
),
),
),
],
),
],
),
buildHeaderButtons(expanded: false),
],
),
],
const SizedBox(height: 16),
if (sorted.isEmpty)
Container(