feat(settings): add teacher due date reminders card and automated notification service
This commit is contained in:
1 parent
ae5246f271
commit
32cff2431a
8 files changed
+541
-80
No files matched your search
@@ -88,6 +88,15 @@ app.listen(PORT, '0.0.0.0', () => {
|
||||
console.error('Failed to start auto-archiving schedule:', err.message);
|
||||
}
|
||||
|
||||
// Run teacher document requirement due date reminders check on startup and hourly
|
||||
try {
|
||||
const { checkAndRunTeacherDueDateReminders } = require('./src/services/teacherReminderService');
|
||||
checkAndRunTeacherDueDateReminders();
|
||||
setInterval(() => checkAndRunTeacherDueDateReminders(), 60 * 60 * 1000);
|
||||
} catch (err) {
|
||||
console.error('Failed to start teacher reminder service:', err.message);
|
||||
}
|
||||
|
||||
// Initialize Auto Cloudflare Tunnel activation when internet is connected
|
||||
try {
|
||||
const { startAutoTunnel } = require('./src/services/tunnelService');
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
const db = require('../config/db');
|
||||
const autoArchiveService = require('../services/autoArchiveService');
|
||||
const { checkAndRunTeacherDueDateReminders } = require('../services/teacherReminderService');
|
||||
|
||||
exports.getSettings = (req, res) => {
|
||||
try {
|
||||
const rows = db.prepare('SELECT key, value FROM system_settings').all();
|
||||
const settings = {
|
||||
auto_update_enrollment_from_sf: 'false',
|
||||
enrollment_grace_period_days: '30'
|
||||
enrollment_grace_period_days: '30',
|
||||
remind_teachers_due_date_enabled: 'false',
|
||||
remind_teachers_due_date_days: '3'
|
||||
};
|
||||
for (const row of rows) {
|
||||
settings[row.key] = row.value;
|
||||
@@ -56,6 +59,10 @@ exports.updateSettings = (req, res) => {
|
||||
autoArchiveService.scheduleAutoArchiveTimer();
|
||||
}
|
||||
|
||||
if (updates.remind_teachers_due_date_enabled !== undefined || updates.remind_teachers_due_date_days !== undefined) {
|
||||
checkAndRunTeacherDueDateReminders();
|
||||
}
|
||||
|
||||
const rows = db.prepare('SELECT key, value FROM system_settings').all();
|
||||
const updatedSettings = {};
|
||||
for (const row of rows) {
|
||||
|
||||
@@ -935,6 +935,14 @@ const initSchema = () => {
|
||||
INSERT OR IGNORE INTO system_settings (key, value)
|
||||
VALUES ('auto_update_enrollment_from_sf', 'false')
|
||||
`).run();
|
||||
db.prepare(`
|
||||
INSERT OR IGNORE INTO system_settings (key, value)
|
||||
VALUES ('remind_teachers_due_date_enabled', 'false')
|
||||
`).run();
|
||||
db.prepare(`
|
||||
INSERT OR IGNORE INTO system_settings (key, value)
|
||||
VALUES ('remind_teachers_due_date_days', '3')
|
||||
`).run();
|
||||
|
||||
// Seed Admin if not exists
|
||||
const adminUser = db.prepare("SELECT * FROM users WHERE username = 'admin' OR role = 'admin'").get();
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
const db = require('../config/db');
|
||||
const { sendTeacherAttentionReminder } = require('./emailService');
|
||||
|
||||
const logActivity = (userId, action, entityType, entityId, description) => {
|
||||
try {
|
||||
let uid = userId;
|
||||
const userCheck = uid ? db.prepare("SELECT id FROM users WHERE id = ?").get(uid) : null;
|
||||
if (!userCheck) {
|
||||
const anyUser = db.prepare("SELECT id FROM users WHERE role = 'admin' LIMIT 1").get();
|
||||
uid = anyUser ? anyUser.id : null;
|
||||
}
|
||||
db.prepare(`
|
||||
INSERT INTO activity_log (user_id, action, entity_type, entity_id, description)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(uid, action, entityType, entityId, description);
|
||||
} catch (err) {
|
||||
console.error('[teacherReminderService] logActivity error:', err.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks document requirements with due dates and sends automated reminder emails
|
||||
* to advisory teachers whose enrolled students are missing those requirements.
|
||||
*/
|
||||
const checkAndRunTeacherDueDateReminders = async (forced = false) => {
|
||||
try {
|
||||
const enabledRow = db.prepare("SELECT value FROM system_settings WHERE key = 'remind_teachers_due_date_enabled'").get();
|
||||
const isEnabled = enabledRow?.value === 'true';
|
||||
|
||||
if (!isEnabled && !forced) {
|
||||
return { executed: false, reason: 'Teacher due date reminders are disabled in system settings.' };
|
||||
}
|
||||
|
||||
const daysRow = db.prepare("SELECT value FROM system_settings WHERE key = 'remind_teachers_due_date_days'").get();
|
||||
const reminderDays = parseInt(daysRow?.value || '3', 10) || 3;
|
||||
|
||||
// Today's date string YYYY-MM-DD
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const todayStr = `${year}-${month}-${day}`;
|
||||
|
||||
// Check if already executed today
|
||||
if (!forced) {
|
||||
const lastRunRow = db.prepare("SELECT value FROM system_settings WHERE key = 'last_teacher_reminder_date'").get();
|
||||
if (lastRunRow?.value === todayStr) {
|
||||
return { executed: false, reason: 'Teacher due date reminders already ran today.' };
|
||||
}
|
||||
}
|
||||
|
||||
// Get enabled document requirements that have a due date set
|
||||
const requirements = db.prepare(`
|
||||
SELECT id, name, category, due_date, is_mandatory
|
||||
FROM document_requirements
|
||||
WHERE is_enabled = 1 AND due_date IS NOT NULL AND TRIM(due_date) != ''
|
||||
`).all();
|
||||
|
||||
if (requirements.length === 0) {
|
||||
if (!forced) {
|
||||
db.prepare(`
|
||||
INSERT INTO system_settings (key, value, updated_at)
|
||||
VALUES ('last_teacher_reminder_date', ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
||||
`).run(todayStr);
|
||||
}
|
||||
return { executed: false, reason: 'No document requirements with due dates found.' };
|
||||
}
|
||||
|
||||
// Filter requirements that are due within reminderDays (0 <= diffDays <= reminderDays)
|
||||
const todayDateOnly = new Date(`${todayStr}T00:00:00`);
|
||||
const dueSoonRequirements = requirements.filter(req => {
|
||||
const reqDateStr = req.due_date.split('T')[0].split(' ')[0];
|
||||
const reqDate = new Date(`${reqDateStr}T00:00:00`);
|
||||
const diffTime = reqDate.getTime() - todayDateOnly.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return diffDays >= 0 && diffDays <= reminderDays;
|
||||
});
|
||||
|
||||
if (dueSoonRequirements.length === 0) {
|
||||
if (!forced) {
|
||||
db.prepare(`
|
||||
INSERT INTO system_settings (key, value, updated_at)
|
||||
VALUES ('last_teacher_reminder_date', ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
||||
`).run(todayStr);
|
||||
}
|
||||
return { executed: false, reason: `No document requirements due within the next ${reminderDays} day(s).` };
|
||||
}
|
||||
|
||||
// Get active teachers with emails
|
||||
const teachers = db.prepare(`
|
||||
SELECT id, username, first_name, middle_name, last_name, email
|
||||
FROM users
|
||||
WHERE role = 'teacher' AND is_active = 1 AND email IS NOT NULL AND TRIM(email) != ''
|
||||
`).all();
|
||||
|
||||
if (teachers.length === 0) {
|
||||
return { executed: false, reason: 'No active teachers with valid emails found.' };
|
||||
}
|
||||
|
||||
let sentCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const teacher of teachers) {
|
||||
const teacherName = `${teacher.first_name} ${teacher.last_name}`.trim();
|
||||
|
||||
// Get sections assigned to this teacher
|
||||
const sections = db.prepare(`
|
||||
SELECT s.id, s.name, s.grade_level
|
||||
FROM teacher_sections ts
|
||||
JOIN sections s ON ts.section_id = s.id
|
||||
WHERE ts.teacher_id = ?
|
||||
ORDER BY s.grade_level ASC, s.name ASC
|
||||
`).all(teacher.id);
|
||||
|
||||
if (sections.length === 0) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const sectionsWithAttentionStudents = [];
|
||||
|
||||
for (const section of sections) {
|
||||
// Find enrolled students in this section
|
||||
const students = db.prepare(`
|
||||
SELECT s.id, s.lrn, s.first_name, s.middle_name, s.last_name, s.extension
|
||||
FROM enrollments e
|
||||
JOIN students s ON e.student_id = s.id
|
||||
WHERE e.section_id = ? AND s.status = 'Enrolled'
|
||||
ORDER BY s.last_name ASC, s.first_name ASC
|
||||
`).all(section.id);
|
||||
|
||||
const sectionAttentionStudents = [];
|
||||
|
||||
for (const student of students) {
|
||||
const studentFullName = [student.last_name, student.first_name, student.middle_name, student.extension]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
// Check missing requirements among the due soon requirements
|
||||
const studentLevelCategory = section.grade_level <= 10 ? 'JHS' : 'SHS';
|
||||
const matchingReqs = dueSoonRequirements.filter(r => r.category === studentLevelCategory);
|
||||
|
||||
if (matchingReqs.length === 0) continue;
|
||||
|
||||
const missingForStudent = [];
|
||||
|
||||
for (const req of matchingReqs) {
|
||||
const hasCompleted = db.prepare(`
|
||||
SELECT id FROM documents
|
||||
WHERE student_id = ? AND requirement_id = ? AND status = 'Completed' AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
`).get(student.id, req.id);
|
||||
|
||||
if (!hasCompleted) {
|
||||
const reqDateStr = req.due_date.split('T')[0].split(' ')[0];
|
||||
missingForStudent.push(`${req.name} (Due: ${reqDateStr})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingForStudent.length > 0) {
|
||||
sectionAttentionStudents.push({
|
||||
id: student.id,
|
||||
lrn: student.lrn,
|
||||
name: studentFullName,
|
||||
missingDocs: missingForStudent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionAttentionStudents.length > 0) {
|
||||
sectionsWithAttentionStudents.push({
|
||||
sectionId: section.id,
|
||||
sectionName: section.name,
|
||||
gradeLevel: section.grade_level,
|
||||
students: sectionAttentionStudents
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionsWithAttentionStudents.length > 0) {
|
||||
try {
|
||||
// Validate teacher email before sending reminder (MEV / QEV)
|
||||
let validEmail = true;
|
||||
try {
|
||||
const { validateEmailMEV } = require('../controllers/authController');
|
||||
const mevResult = await validateEmailMEV(teacher.email);
|
||||
if (!mevResult.valid) {
|
||||
validEmail = false;
|
||||
console.warn(`[TeacherReminderService] Skipping ${teacher.email}: ${mevResult.reason}`);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (!validEmail) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
await sendTeacherAttentionReminder({
|
||||
to: teacher.email,
|
||||
teacherName,
|
||||
sectionsWithStudents: sectionsWithAttentionStudents
|
||||
});
|
||||
sentCount++;
|
||||
} catch (err) {
|
||||
console.error(`[TeacherReminderService] Failed to send reminder email to ${teacher.email}:`, err.message);
|
||||
}
|
||||
} else {
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark today as executed
|
||||
db.prepare(`
|
||||
INSERT INTO system_settings (key, value, updated_at)
|
||||
VALUES ('last_teacher_reminder_date', ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
||||
`).run(todayStr);
|
||||
|
||||
if (sentCount > 0) {
|
||||
logActivity(
|
||||
null,
|
||||
'NOTIFY',
|
||||
'system_settings',
|
||||
null,
|
||||
`Automated due date reminder emails sent to ${sentCount} teacher(s)`
|
||||
);
|
||||
console.log(`[TeacherReminderService] Successfully sent due date reminder emails to ${sentCount} teacher(s).`);
|
||||
}
|
||||
|
||||
return {
|
||||
executed: true,
|
||||
sentCount,
|
||||
skippedCount,
|
||||
dueRequirementsCount: dueSoonRequirements.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[TeacherReminderService] checkAndRunTeacherDueDateReminders error:', error);
|
||||
return { executed: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
checkAndRunTeacherDueDateReminders
|
||||
};
|
||||
+19
-2
@@ -847,13 +847,30 @@ Files backup/restore - mirror mode or zip -->
|
||||
# SETTINGS SCREEN
|
||||
- make the fade at top and bottom fade show when it has item under it if not dont show -->
|
||||
|
||||
<!-- # SETTINGS / TEACHER REMINDERS
|
||||
- settings screen -> add new collapsible card "Remind Teachers" for document requirements due date:
|
||||
- settings toggle to enable/disable automated email notifications to teachers before due date
|
||||
- configurable days input/selector (default: 3 days before due date, can manually change)
|
||||
- sends email notification to advisory teachers with list of missing students per section
|
||||
- teachers & academic setup screen -> remove "Remind All Teachers" button -->
|
||||
|
||||
# CSV BULK IMPORT ENHANCEMENTS
|
||||
- bulk student import (add bulk student modal / screen):
|
||||
- add on-screen CSV format example & template guide (headers: LRN, First Name, Middle Name, Last Name, Extension, Sex, Birth Date, 4Ps)
|
||||
- add live type / paste CSV input area with "Parse & Preview" to submit directly without needing a separate file
|
||||
- validation table / live preview showing valid rows and detected errors before confirming bulk import
|
||||
- academic & sections setup (teachers & academic setup screen):
|
||||
- add bulk CSV import for academic structure:
|
||||
- format: Academic Year, Grade Level, Section Name (e.g., `2024-2025, 10, Diamond`)
|
||||
- add on-screen CSV format example / reference & sample rows
|
||||
- add live type / paste CSV input area with instant parse, validation preview, and bulk submit
|
||||
|
||||
---
|
||||
|
||||
|
||||
# DONT DO
|
||||
<!-- - documents screen -> add how many files can be uploaded into a document requirement, the current limitation is only 1, so we can dynamically choose number files that can be uploaded into that document, update both frontend and backend, but first give me suggestion where will put the setting of this, ask me first -->
|
||||
|
||||
- CSV bulk add student, academic and section [acad year, grade level, sections]
|
||||
|
||||
- print list history, print email to the student make it optional, add date of print, add status (done, not done, in progress, picked up), add message to email can be manual message typed by registrar/staff,
|
||||
|
||||
- teacher no assigned section theme color
|
||||
|
||||
@@ -22,6 +22,7 @@ import '../../../domain/entities/setup_models.dart';
|
||||
import '../../providers/system_settings_provider.dart';
|
||||
import '../../providers/theme_provider.dart';
|
||||
import '../../providers/preferences_provider.dart';
|
||||
import '../../providers/users_provider.dart';
|
||||
import '../../../core/services/haptic_service.dart';
|
||||
import 'security_screen.dart';
|
||||
class TitleCaseTextInputFormatter extends TextInputFormatter {
|
||||
@@ -120,12 +121,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
bool _isProfileExpanded = false;
|
||||
bool _isAcademicYearExpanded = false;
|
||||
bool _isAutoEnrollExpanded = false;
|
||||
bool _isTeacherRemindersExpanded = false;
|
||||
bool _isAppearanceExpanded = false;
|
||||
|
||||
void _collapseAllSections() {
|
||||
_isProfileExpanded = false;
|
||||
_isAcademicYearExpanded = false;
|
||||
_isAutoEnrollExpanded = false;
|
||||
_isTeacherRemindersExpanded = false;
|
||||
_isAppearanceExpanded = false;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _onScroll();
|
||||
@@ -517,6 +520,63 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isSendingManualReminder = false;
|
||||
|
||||
Future<void> _handleManualRemindTeachers() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.mark_email_read_outlined, color: AppColors.primaryGreen, size: 24),
|
||||
SizedBox(width: 8),
|
||||
Expanded(child: Text('Send Teacher Reminders')),
|
||||
],
|
||||
),
|
||||
content: const Text(
|
||||
'Send reminder emails to all active advisory teachers with the list of students in their assigned sections who currently have missing document requirements?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('CANCEL'),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('SEND REMINDERS'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
setState(() => _isSendingManualReminder = true);
|
||||
try {
|
||||
final res = await ref.read(usersProvider.notifier).remindTeachers();
|
||||
if (!mounted) return;
|
||||
showSuccessDialog(
|
||||
context,
|
||||
title: 'Reminders Sent',
|
||||
message: res['message']?.toString() ?? 'Teacher reminders sent successfully.',
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showErrorDialog(
|
||||
context,
|
||||
'Reminder Failed',
|
||||
e.toString().replaceAll('Exception: ', ''),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSendingManualReminder = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.listen<String>(activeTabProvider, (previous, next) {
|
||||
@@ -1496,6 +1556,196 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: AppSizes.p24),
|
||||
|
||||
// ── Remind Teachers (Admin Only) ──────────────────────
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final sysSettingsAsync = ref.watch(systemSettingsProvider);
|
||||
final settingsMap = sysSettingsAsync.asData?.value ?? {};
|
||||
final isRemindEnabled =
|
||||
(settingsMap['remind_teachers_due_date_enabled'] ?? 'false') == 'true';
|
||||
final reminderDays =
|
||||
int.tryParse(settingsMap['remind_teachers_due_date_days'] ?? '3') ?? 3;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return _buildCollapsibleCard(
|
||||
title: 'Remind Teachers',
|
||||
isExpanded: _isTeacherRemindersExpanded,
|
||||
onToggle: () => _toggleSection(
|
||||
() => _isTeacherRemindersExpanded = !_isTeacherRemindersExpanded,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.mark_email_read_outlined,
|
||||
color: AppColors.primaryGreen,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: AppSizes.p12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Automated Due Date Reminders',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Notify teachers via email before document requirement due dates arrive',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? AppColors.darkTextSecondary : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: isRemindEnabled,
|
||||
activeThumbColor: AppColors.primaryGreen,
|
||||
onChanged: (val) {
|
||||
ref
|
||||
.read(systemSettingsProvider.notifier)
|
||||
.updateSetting(
|
||||
'remind_teachers_due_date_enabled',
|
||||
val ? 'true' : 'false',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'When enabled, advisory teachers will automatically receive an email listing the students in their assigned section(s) who currently have missing document requirements before the due date.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? AppColors.darkTextPrimary : AppColors.primaryGreen,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
Text(
|
||||
'Reminder Schedule',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Number of days before the requirement due date to send reminder emails:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? AppColors.darkTextSecondary : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSizes.p12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [1, 2, 3, 5, 7].map((days) {
|
||||
final isSelected = reminderDays == days;
|
||||
return ChoiceChip(
|
||||
label: Text(
|
||||
days == 3 ? '$days Days (Default)' : '$days Day${days > 1 ? 's' : ''}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: (isDark ? AppColors.darkTextPrimary : AppColors.textPrimary),
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
selectedColor: AppColors.primaryGreen,
|
||||
backgroundColor: isDark ? AppColors.darkSurface2 : Colors.grey.shade100,
|
||||
showCheckmark: false,
|
||||
onSelected: (_) {
|
||||
ref
|
||||
.read(systemSettingsProvider.notifier)
|
||||
.updateSetting(
|
||||
'remind_teachers_due_date_days',
|
||||
days.toString(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
Divider(color: isDark ? AppColors.darkBorder : Colors.grey.shade200),
|
||||
const SizedBox(height: AppSizes.p12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Need to remind teachers right away? You can trigger a manual reminder dispatch at any time.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? AppColors.darkTextSecondary : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isSendingManualReminder ? null : _handleManualRemindTeachers,
|
||||
icon: _isSendingManualReminder
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: const Icon(Icons.send_rounded, size: 15),
|
||||
label: Text(_isSendingManualReminder ? 'Sending...' : 'Send Reminders Now'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
textStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: AppSizes.p24),
|
||||
],
|
||||
|
||||
// ── Appearance & Feedback Card (Collapsible) ───────────
|
||||
|
||||
@@ -167,63 +167,6 @@ class _TeachersTab extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _TeachersTabState extends ConsumerState<_TeachersTab> {
|
||||
bool _isSending = false;
|
||||
|
||||
Future<void> _handleRemindAll() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.mark_email_read_outlined, color: AppColors.primaryGreen, size: 24),
|
||||
SizedBox(width: 8),
|
||||
Expanded(child: Text('Remind All Teachers')),
|
||||
],
|
||||
),
|
||||
content: const Text(
|
||||
'Send reminder emails to all active teachers with the list of students in their assigned sections who currently have missing mandatory documents?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('CANCEL'),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('SEND REMINDERS'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
setState(() => _isSending = true);
|
||||
try {
|
||||
final res = await ref.read(usersProvider.notifier).remindTeachers();
|
||||
if (!mounted) return;
|
||||
showSuccessDialog(
|
||||
context,
|
||||
title: 'Reminders Sent',
|
||||
message: res['message']?.toString() ?? 'Teacher reminders sent successfully.',
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showErrorDialog(
|
||||
context,
|
||||
'Reminder Failed',
|
||||
e.toString().replaceAll('Exception: ', ''),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSending = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final usersAsync = ref.watch(usersProvider);
|
||||
@@ -274,25 +217,6 @@ class _TeachersTabState extends ConsumerState<_TeachersTab> {
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isSending ? null : _handleRemindAll,
|
||||
icon: _isSending
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: const Icon(Icons.mark_email_read_outlined, size: 16),
|
||||
label: Text(_isSending ? 'Sending...' : 'Remind All Teachers'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
textStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.26+26
|
||||
version: 1.0.27+27
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.5
|
||||
|
||||
Reference in new issue
Block a user