feat(settings): clean academic setup buttons and rename screen to Academic & Class Management

- Update screen, settings card, and dashboard titles to 'Academic & Class Management'

- Clean and balance Academic Structure toolbar for mobile and desktop

- Remove redundant buttons on section cards in favor of interactive adviser chip

- Modernize teacher action button to OutlinedButton.icon 'Sections'

- Add responsive bulk academic structure import support across backend and frontend
This commit is contained in:
ssbg04 committed 2026-09-27 02:05:47 +08:00
1 parent a9c2ab0da5
commit bd2a8acbfe
8 files changed
+1228 -454

No files matched your search

+59 -9
View File
@@ -193,9 +193,14 @@ exports.deleteAcademicYear = (req, res) => {
exports.getAllSections = (req, res) => {
try {
const sections = db.prepare(`
SELECT s.*, ay.year_range as academic_year_range
SELECT s.*, ay.year_range as academic_year_range,
ts.teacher_id,
u.first_name as teacher_first_name,
u.last_name as teacher_last_name
FROM sections s
LEFT JOIN academic_years ay ON s.academic_year_id = ay.id
LEFT JOIN teacher_sections ts ON s.id = ts.section_id
LEFT JOIN users u ON ts.teacher_id = u.id
ORDER BY ay.year_range DESC, s.grade_level ASC, s.name ASC
`).all();
res.json(sections);
@@ -206,7 +211,18 @@ exports.getAllSections = (req, res) => {
exports.getSectionsByYear = (req, res) => {
try {
const sections = db.prepare('SELECT * FROM sections WHERE academic_year_id = ? ORDER BY grade_level ASC, name ASC').all(req.params.yearId);
const sections = db.prepare(`
SELECT s.*, ay.year_range as academic_year_range,
ts.teacher_id,
u.first_name as teacher_first_name,
u.last_name as teacher_last_name
FROM sections s
LEFT JOIN academic_years ay ON s.academic_year_id = ay.id
LEFT JOIN teacher_sections ts ON s.id = ts.section_id
LEFT JOIN users u ON ts.teacher_id = u.id
WHERE s.academic_year_id = ?
ORDER BY s.grade_level ASC, s.name ASC
`).all(req.params.yearId);
res.json(sections);
} catch (error) {
res.status(500).json({ message: 'Failed to fetch sections', error: error.message });
@@ -214,14 +230,21 @@ exports.getSectionsByYear = (req, res) => {
};
exports.createSection = (req, res) => {
const { name, gradeLevel, academicYearId } = req.body;
const { name, gradeLevel, academicYearId, teacherId } = req.body;
if (!name || !name.trim() || !gradeLevel || !academicYearId) {
return res.status(400).json({ message: 'name, gradeLevel, and academicYearId are required' });
}
try {
const result = db.prepare('INSERT INTO sections (name, grade_level, academic_year_id) VALUES (?, ?, ?)')
.run(name.trim(), gradeLevel, academicYearId);
res.status(201).json({ id: result.lastInsertRowid, message: 'Section created successfully' });
let sectionId;
db.transaction(() => {
const result = db.prepare('INSERT INTO sections (name, grade_level, academic_year_id) VALUES (?, ?, ?)')
.run(name.trim(), gradeLevel, academicYearId);
sectionId = result.lastInsertRowid;
if (teacherId) {
db.prepare('INSERT INTO teacher_sections (teacher_id, section_id) VALUES (?, ?)').run(teacherId, sectionId);
}
})();
res.status(201).json({ id: sectionId, message: 'Section created successfully' });
} catch (error) {
res.status(500).json({ message: 'Failed to create section', error: error.message });
}
@@ -229,7 +252,7 @@ exports.createSection = (req, res) => {
exports.updateSection = (req, res) => {
const { id } = req.params;
const { name, gradeLevel, academicYearId } = req.body;
const { name, gradeLevel, academicYearId, teacherId } = req.body;
if (!name || !name.trim() || !gradeLevel || !academicYearId) {
return res.status(400).json({ message: 'name, gradeLevel, and academicYearId are required' });
@@ -239,14 +262,41 @@ exports.updateSection = (req, res) => {
const section = db.prepare('SELECT id FROM sections WHERE id = ?').get(id);
if (!section) return res.status(404).json({ message: 'Section not found' });
db.prepare('UPDATE sections SET name = ?, grade_level = ?, academic_year_id = ? WHERE id = ?')
.run(name.trim(), gradeLevel, academicYearId, id);
db.transaction(() => {
db.prepare('UPDATE sections SET name = ?, grade_level = ?, academic_year_id = ? WHERE id = ?')
.run(name.trim(), gradeLevel, academicYearId, id);
if (teacherId !== undefined) {
db.prepare('DELETE FROM teacher_sections WHERE section_id = ?').run(id);
if (teacherId) {
db.prepare('INSERT INTO teacher_sections (teacher_id, section_id) VALUES (?, ?)').run(teacherId, id);
}
}
})();
res.json({ message: 'Section updated successfully' });
} catch (error) {
res.status(500).json({ message: 'Failed to update section', error: error.message });
}
};
exports.setSectionAdviser = (req, res) => {
const { id } = req.params;
const { teacherId } = req.body;
try {
const section = db.prepare('SELECT id FROM sections WHERE id = ?').get(id);
if (!section) return res.status(404).json({ message: 'Section not found' });
db.transaction(() => {
db.prepare('DELETE FROM teacher_sections WHERE section_id = ?').run(id);
if (teacherId) {
db.prepare('INSERT INTO teacher_sections (teacher_id, section_id) VALUES (?, ?)').run(teacherId, id);
}
})();
res.json({ message: 'Section adviser updated successfully' });
} catch (error) {
res.status(500).json({ message: 'Failed to update section adviser', error: error.message });
}
};
exports.deleteSection = (req, res) => {
const { id } = req.params;
try {
+1
View File
@@ -21,6 +21,7 @@ router.get('/academic-years/:yearId/sections', authenticateToken, setupControlle
router.post('/sections', authenticateToken, authorizeRoles('admin'), setupController.createSection);
router.post('/bulk-academic-structure', authenticateToken, authorizeRoles('admin'), setupController.bulkCreateAcademicStructure);
router.put('/sections/:id', authenticateToken, authorizeRoles('admin'), setupController.updateSection);
router.put('/sections/:id/adviser', authenticateToken, authorizeRoles('admin'), setupController.setSectionAdviser);
router.delete('/sections/:id', authenticateToken, authorizeRoles('admin'), setupController.deleteSection);
// ==========================================
@@ -30,6 +30,16 @@ class SectionModel {
final int gradeLevel;
final int? academicYearId;
final String? academicYearRange;
final int? teacherId;
final String? teacherFirstName;
final String? teacherLastName;
String? get teacherFullName {
if (teacherLastName != null && teacherFirstName != null) {
return '$teacherLastName, $teacherFirstName';
}
return teacherFirstName ?? teacherLastName;
}
SectionModel({
required this.id,
@@ -37,6 +47,9 @@ class SectionModel {
required this.gradeLevel,
this.academicYearId,
this.academicYearRange,
this.teacherId,
this.teacherFirstName,
this.teacherLastName,
});
factory SectionModel.fromJson(Map<String, dynamic> json) {
@@ -48,6 +61,11 @@ class SectionModel {
? (json['academic_year_id'] as num).toInt()
: null,
academicYearRange: json['academic_year_range'] as String?,
teacherId: json['teacher_id'] != null
? (json['teacher_id'] as num).toInt()
: null,
teacherFirstName: json['teacher_first_name'] as String?,
teacherLastName: json['teacher_last_name'] as String?,
);
}
}
@@ -167,6 +167,7 @@ class SetupRepository {
required String name,
required int gradeLevel,
required int academicYearId,
int? teacherId,
}) async {
try {
final options = await _getAuthOptions();
@@ -177,6 +178,7 @@ class SetupRepository {
'name': name,
'gradeLevel': gradeLevel,
'academicYearId': academicYearId,
if (teacherId != null) 'teacherId': teacherId,
},
);
} on DioException catch (e) {
@@ -208,6 +210,7 @@ class SetupRepository {
required String name,
required int gradeLevel,
required int academicYearId,
int? teacherId,
}) async {
try {
final options = await _getAuthOptions();
@@ -218,6 +221,7 @@ class SetupRepository {
'name': name,
'gradeLevel': gradeLevel,
'academicYearId': academicYearId,
'teacherId': teacherId,
},
);
} on DioException catch (e) {
@@ -225,6 +229,22 @@ class SetupRepository {
}
}
Future<void> setSectionAdviser({
required int sectionId,
int? teacherId,
}) async {
try {
final options = await _getAuthOptions();
await _dio.put(
'/setup/sections/$sectionId/adviser',
options: options,
data: {'teacherId': teacherId},
);
} on DioException catch (e) {
throw Exception(_extractErrorMessage(e, 'Failed to update section adviser.'));
}
}
Future<void> deleteSection(int id) async {
try {
final options = await _getAuthOptions();
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/setup_models.dart';
import '../../domain/repositories/setup_repository.dart';
import 'auth_provider.dart';
import 'users_provider.dart';
final setupRepositoryProvider = Provider<SetupRepository>((ref) {
return SetupRepository();
@@ -134,6 +135,7 @@ class SetupMutationNotifier extends AsyncNotifier<void> {
required String name,
required int gradeLevel,
required int academicYearId,
int? teacherId,
}) async {
state = const AsyncLoading();
try {
@@ -142,9 +144,11 @@ class SetupMutationNotifier extends AsyncNotifier<void> {
name: name,
gradeLevel: gradeLevel,
academicYearId: academicYearId,
teacherId: teacherId,
);
state = const AsyncData(null);
ref.invalidate(sectionsListProvider);
ref.invalidate(usersProvider);
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
@@ -162,6 +166,7 @@ class SetupMutationNotifier extends AsyncNotifier<void> {
ref.invalidate(academicYearsListProvider);
ref.invalidate(gradeLevelsListProvider);
ref.invalidate(sectionsListProvider);
ref.invalidate(usersProvider);
return res;
} catch (e, st) {
state = AsyncError(e, st);
@@ -174,6 +179,7 @@ class SetupMutationNotifier extends AsyncNotifier<void> {
required String name,
required int gradeLevel,
required int academicYearId,
int? teacherId,
}) async {
state = const AsyncLoading();
try {
@@ -183,9 +189,28 @@ class SetupMutationNotifier extends AsyncNotifier<void> {
name: name,
gradeLevel: gradeLevel,
academicYearId: academicYearId,
teacherId: teacherId,
);
state = const AsyncData(null);
ref.invalidate(sectionsListProvider);
ref.invalidate(usersProvider);
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
Future<void> setSectionAdviser({
required int sectionId,
int? teacherId,
}) async {
state = const AsyncLoading();
try {
final repo = ref.read(setupRepositoryProvider);
await repo.setSectionAdviser(sectionId: sectionId, teacherId: teacherId);
state = const AsyncData(null);
ref.invalidate(sectionsListProvider);
ref.invalidate(usersProvider);
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
@@ -571,8 +571,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
children: [
_setupNavButton(
context,
icon: Icons.people_outline,
label: 'Teachers & Academic Setup',
icon: Icons.school_outlined,
label: 'Academic & Class Management',
isMobile: true,
onTap: () => TeacherManagementModal.open(context),
),
@@ -626,8 +626,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
children: [
_setupNavButton(
context,
icon: Icons.people_outline,
label: 'Teachers & Academic Setup',
icon: Icons.school_outlined,
label: 'Academic & Class Management',
onTap: () =>
TeacherManagementModal.open(context),
),
@@ -990,7 +990,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const Row(
children: [
Icon(
Icons.people_alt,
Icons.school_outlined,
color: AppColors.primaryGreen,
),
SizedBox(width: AppSizes.p8),
@@ -1000,14 +1000,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
CrossAxisAlignment.start,
children: [
Text(
'Teachers & Academic Setup',
'Academic & Class Management',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Manage teachers, academic years, grade levels, and sections',
'Manage academic years, sections, and class advisers',
style: TextStyle(
fontSize: 13,
color: Colors.grey,
@@ -1112,7 +1112,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
);
if (years.isEmpty) {
return const Text(
'No academic years found. Configure an academic year in Teachers & Academic Setup first.',
'No academic years found. Configure an academic year in Academic & Class Management first.',
style: TextStyle(color: AppColors.warning, fontSize: 13),
);
}
@@ -1129,7 +1129,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
border: Border.all(color: AppColors.warning.withValues(alpha: 0.3)),
),
child: const Text(
'No active academic year found. An administrator must activate a school year in Teachers & Academic Setup.',
'No active academic year found. An administrator must activate a school year in Academic & Class Management.',
style: TextStyle(color: AppColors.warning, fontSize: 13),
),
),
File diff suppressed because it is too large. Load diff