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:
1 parent
a9c2ab0da5
commit
bd2a8acbfe
8 files changed
+1032
-258
No files matched your search
@@ -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 {
|
||||
let sectionId;
|
||||
db.transaction(() => {
|
||||
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' });
|
||||
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.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 {
|
||||
|
||||
@@ -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
Reference in new issue
Block a user